import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:result_monad/result_monad.dart'; import '../controls/image_control.dart'; import '../controls/padding.dart'; import '../controls/responsive_max_width.dart'; import '../controls/standard_appbar.dart'; import '../controls/status_and_refresh_button.dart'; import '../models/auth/profile.dart'; import '../models/direct_message.dart'; import '../models/direct_message_thread.dart'; import '../riverpod_controllers/account_services.dart'; import '../riverpod_controllers/connection_manager_services.dart'; import '../riverpod_controllers/direct_message_services.dart'; import '../riverpod_controllers/networking/network_status_services.dart'; import '../utils/clipboard_utils.dart'; import '../utils/snackbar_builder.dart'; class MessageThreadScreen extends ConsumerStatefulWidget { final String parentThreadId; const MessageThreadScreen({ super.key, required this.parentThreadId, }); @override ConsumerState createState() => _MessageThreadScreenState(); } class _MessageThreadScreenState extends ConsumerState { final textController = TextEditingController(); @override void initState() { super.initState(); ref .read(directMessageThreadServiceProvider( ref.read(activeProfileProvider), widget.parentThreadId, ).notifier) .refresh(); } @override Widget build(BuildContext context) { final profile = ref.watch(activeProfileProvider); final loading = ref.watch(directMessageLoadingProvider(profile)); final thread = ref.watch( directMessageThreadServiceProvider(profile, widget.parentThreadId)); final title = thread.title.isEmpty ? 'Thread' : thread.title; return Scaffold( appBar: StandardAppBar.build(context, title, actions: [ StatusAndRefreshButton( executing: loading, refreshFunction: () async => await ref .read(directMessageThreadServiceProvider( profile, widget.parentThreadId, ).notifier) .refresh(), busyColor: Theme.of(context).colorScheme.surface, ), ]), body: buildBody(profile, thread, loading), ); } Widget buildBody( Profile profile, DirectMessageThread thread, bool loading, ) { return Center( child: Column( children: [ if (loading) const LinearProgressIndicator(), Expanded( child: ResponsiveMaxWidth( child: ListView.separated( itemBuilder: (context, index) { final m = thread.messages[index]; return _DirectMessageListItem( m, profile, widget.parentThreadId); }, separatorBuilder: (_, __) => const Divider(), itemCount: thread.messages.length), ), ), const VerticalDivider(), Padding( padding: const EdgeInsets.all(8.0), child: ResponsiveMaxWidth( child: TextFormField( controller: textController, textCapitalization: TextCapitalization.sentences, spellCheckConfiguration: const SpellCheckConfiguration(), maxLines: 4, decoration: InputDecoration( labelText: 'Reply Text', border: OutlineInputBorder( borderSide: BorderSide( color: Theme.of(context).colorScheme.surface, ), borderRadius: BorderRadius.circular(5.0), ), ), ), ), ), ElevatedButton( onPressed: () async { if (textController.text.isEmpty) { buildSnackbar(context, "Can't submit an empty reply"); return; } final othersMessages = thread.messages.where((m) => m.senderId != profile.userId); if (othersMessages.isEmpty) { buildSnackbar( context, "Have to wait for a response before sending"); return; } await ref .read(directMessageThreadServiceProvider( profile, thread.parentUri) .notifier) .newReplyMessage( othersMessages.last, textController.text, ) .match(onSuccess: (_) { setState(() { textController.clear(); }); }, onError: (error) { if (mounted) { buildSnackbar(context, error.message); } }); }, child: const Text('Submit'), ), const VerticalPadding(), ], )); } } class _DirectMessageListItem extends ConsumerWidget { final DirectMessage message; final Profile profile; final String threadId; const _DirectMessageListItem(this.message, this.profile, this.threadId); @override Widget build(BuildContext context, WidgetRef ref) { final senderAvatar = ref .read(connectionByIdProvider(profile, message.senderId)) .fold(onSuccess: (c) => c.avatarUrl, onError: (_) => ''); final m = message; final textPieces = m.text.split('...\n'); final text = textPieces.length == 1 ? textPieces[0] : textPieces[1]; final imageUrl = m.senderId == profile.userId ? profile.avatar : senderAvatar; return ListTile( onTap: m.seen ? null : () => ref .read(DirectMessageThreadServiceProvider(profile, threadId) .notifier) .markMessageRead(m), onLongPress: () async { await copyToClipboard(context: context, text: m.text); }, leading: ImageControl( imageUrl: imageUrl, iconOverride: const Icon(Icons.person), width: 32.0, onTap: null, ), title: Text( text, style: m.seen ? null : const TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text( DateTime.fromMillisecondsSinceEpoch(m.createdAt * 1000).toString()), ); } }