import "package:cross_cache/cross_cache.dart"; import "package:flutter/material.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_chat_ui/flutter_chat_ui.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:flutter_link_previewer/flutter_link_previewer.dart"; import "package:flyer_chat_file_message/flyer_chat_file_message.dart"; import "package:flyer_chat_image_message/flyer_chat_image_message.dart"; import "package:flyer_chat_system_message/flyer_chat_system_message.dart"; import "package:flyer_chat_text_message/flyer_chat_text_message.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/cross_cache_controller.dart"; import "package:nexus/controllers/selected_room_controller.dart"; import "package:nexus/controllers/room_chat_controller.dart"; import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/extensions/get_headers.dart"; import "package:nexus/helpers/extensions/show_context_menu.dart"; import "package:nexus/models/relation_type.dart"; import "package:nexus/widgets/chat_page/chat_box.dart"; import "package:nexus/widgets/chat_page/html/html.dart"; import "package:nexus/widgets/chat_page/member_list.dart"; import "package:nexus/widgets/chat_page/room_appbar.dart"; import "package:nexus/widgets/chat_page/top_widget.dart"; import "package:nexus/widgets/form_text_input.dart"; import "package:nexus/widgets/loading.dart"; // import "package:dynamic_polls/dynamic_polls.dart"; // import "package:matrix/matrix.dart"; class RoomChat extends HookConsumerWidget { final bool isDesktop; final bool showMembersByDefault; const RoomChat({ required this.isDesktop, required this.showMembersByDefault, super.key, }); @override Widget build(BuildContext context, WidgetRef ref) { final replyToMessage = useState(null); final memberListOpened = useState(showMembersByDefault); final relationType = useState(RelationType.reply); final theme = Theme.of(context); final danger = theme.colorScheme.error; return ref .watch(SelectedRoomController.provider) .betterWhen( data: (room) { if (room == null) { return Center( child: Text( "Nothing to see here...", style: theme.textTheme.headlineMedium, ), ); } final controllerProvider = RoomChatController.provider( room.roomData, ); final notifier = ref.watch(controllerProvider.notifier); List getMessageOptions(Message message) => [ PopupMenuItem( onTap: () { replyToMessage.value = message; relationType.value = RelationType.reply; }, child: ListTile( leading: Icon(Icons.reply), title: Text("Reply"), ), ), // Should check if is state event (has state_key), if so, don't show edit option if (message is TextMessage && message.authorId == room.roomData.client.userID) PopupMenuItem( onTap: () { replyToMessage.value = message; relationType.value = RelationType.edit; }, child: ListTile( leading: Icon(Icons.edit), title: Text("Edit"), ), ), if (message.authorId == room.roomData.client.userID || room.roomData.canRedact) PopupMenuItem( onTap: () => showDialog( context: context, builder: (context) => HookBuilder( builder: (_) { final deleteReasonController = useTextEditingController(); return AlertDialog( title: Text("Delete Message"), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Are you sure you want to delete this message? This can not be reversed.", ), SizedBox(height: 12), FormTextInput( required: false, capitalize: true, controller: deleteReasonController, title: "Reason for deletion (optional)", ), ], ), actions: [ TextButton( onPressed: Navigator.of(context).pop, child: Text("Cancel"), ), TextButton( onPressed: () async { notifier.deleteMessage( message, reason: deleteReasonController.text, ); Navigator.of(context).pop(); }, child: Text("Delete"), ), ], ); }, ), ), child: ListTile( leading: Icon(Icons.delete), title: Text("Delete"), ), ), PopupMenuItem( onTap: () => showDialog( context: context, builder: (context) => AlertDialog( title: Text("Report"), content: Text( "Report this message to your server administrators, who can take action like banning that user or blocking that server from federating.", ), actions: [ TextButton( onPressed: Navigator.of(context).pop, child: Text("Cancel"), ), TextButton( onPressed: () { room.roomData.client.reportEvent( room.roomData.id, message.id, ); Navigator.of(context).pop(); }, child: Text("Report"), ), ], ), ), child: ListTile( leading: Icon(Icons.report, color: danger), title: Text("Report", style: TextStyle(color: danger)), ), ), ]; return Scaffold( appBar: RoomAppbar( room, isDesktop: isDesktop, onOpenDrawer: (_) => Scaffold.of(context).openDrawer(), onOpenMemberList: (thisContext) { memberListOpened.value = !memberListOpened.value; Scaffold.of(thisContext).openEndDrawer(); }, ), body: Row( children: [ Expanded( child: Column( children: [ Expanded( child: ref .watch(controllerProvider) .betterWhen( data: (controller) => Chat( currentUserId: room.roomData.client.userID!, theme: ChatTheme.fromThemeData(theme) .copyWith( colors: ChatColors.fromThemeData(theme) .copyWith( primary: theme .colorScheme .primaryContainer, onPrimary: theme .colorScheme .onPrimaryContainer, ), ), onMessageSecondaryTap: ( context, message, { required index, TapUpDetails? details, }) => details?.globalPosition == null ? null : context.showContextMenu( globalPosition: details!.globalPosition, children: getMessageOptions(message), ), onMessageLongPress: ( context, message, { required details, required index, }) => context.showContextMenu( globalPosition: details.globalPosition, children: getMessageOptions(message), ), onMessageTap: ( context, message, { required details, required index, }) { if (message is ImageMessage) { showDialog( context: context, builder: (_) => Dialog( backgroundColor: Colors.transparent, insetPadding: EdgeInsets.all(64), child: InteractiveViewer( child: Image( image: CachedNetworkImage( message.source, ref.watch( CrossCacheController .provider, ), headers: room .roomData .client .headers, ), ), ), ), ); } }, builders: Builders( loadMoreBuilder: (_) => Loading(), chatAnimatedListBuilder: (_, itemBuilder) => ChatAnimatedList( itemBuilder: itemBuilder, onEndReached: notifier.loadOlder, onStartReached: notifier.markRead, bottomPadding: 72, ), composerBuilder: (_) => ChatBox( relationType: relationType.value, relatedMessage: replyToMessage.value, onDismiss: () => replyToMessage.value = null, room: room.roomData, ), // customMessageBuilder: // ( // context, // message, // index, { // required bool isSentByMe, // MessageGroupStatus? groupStatus, // }) { // final poll = // message.metadata?["poll"] // as PollStartContent; // final responses = // (message.metadata?["responses"] // as Map< // String, // Set // >) // .values // .expand((set) => set) // .fold({}, ( // acc, // value, // ) { // acc[value] = // (acc[value] ?? 0) + 1; // return acc; // }); // return Column( // crossAxisAlignment: // CrossAxisAlignment.start, // spacing: 4, // children: [ // TopWidget( // message, // headers: room // .roomData // .client // .headers, // groupStatus: groupStatus, // ), // // TODO: Make this actually work // DynamicPolls( // startDate: DateTime.now(), // endDate: DateTime.now(), // private: // poll.kind == // PollKind.undisclosed, // allowReselection: true, // backgroundDecoration: // BoxDecoration( // borderRadius: // BorderRadius.all( // Radius.circular(16), // ), // border: Border.all( // color: theme // .colorScheme // .primaryContainer, // width: 4, // ), // ), // allStyle: Styles( // titleStyle: TitleStyle( // style: theme // .textTheme // .headlineSmall, // ), // optionStyle: OptionStyle( // fillColor: theme // .colorScheme // .primaryContainer, // selectedBorderColor: theme // .colorScheme // .primary, // borderColor: theme // .colorScheme // .primary, // unselectedBorderColor: // Colors.transparent, // textSelectColor: theme // .colorScheme // .primary, // ), // ), // onOptionSelected: // (int index) {}, // title: poll.question.mText, // options: poll.answers // .map( // (option) => option.mText, // ) // .toList(), // ), // ], // ); // }, textMessageBuilder: ( context, message, index, { required bool isSentByMe, MessageGroupStatus? groupStatus, }) => FlyerChatTextMessage( customWidget: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Html( (message.metadata?["formatted"] as String) .replaceAllMapped( RegExp( "(]*>.*?<\\/a>)|(\\bhttps?:\\/\\/[^\\s<]+)", caseSensitive: false, ), (m) { // If it's already an tag, leave it unchanged if (m.group(1) != null) { return m.group(1)!; } // Otherwise, wrap the bare URL final url = m.group(2)!; return "$url"; }, ) .replaceAll("\n", "
"), client: room.roomData.client, ), if (message.editedAt != null) Text( "(edited)", style: theme .textTheme .labelSmall, ), ], ), topWidget: TopWidget( message, headers: room.roomData.client.headers, groupStatus: groupStatus, ), message: message, showTime: true, index: index, ), linkPreviewBuilder: (_, message, isSentByMe) => LinkPreview( text: message.text, backgroundColor: isSentByMe ? theme.colorScheme.inversePrimary : theme .colorScheme .surfaceContainerLow, insidePadding: EdgeInsets.symmetric( vertical: 8, horizontal: 16, ), linkPreviewData: message.linkPreviewData, onLinkPreviewDataFetched: (linkPreviewData) => notifier.updateMessage( message, message.copyWith( linkPreviewData: linkPreviewData, ), ), ), imageMessageBuilder: ( _, message, index, { required bool isSentByMe, MessageGroupStatus? groupStatus, }) => FlyerChatImageMessage( topWidget: TopWidget( message, headers: room.roomData.client.headers, groupStatus: groupStatus, alwaysShow: true, ), customImageProvider: CachedNetworkImage( message.source, ref.watch( CrossCacheController.provider, ), headers: room .roomData .client .headers, ), errorBuilder: (context, error, stackTrace) => Center( child: Text( "Image Failed to Load", style: TextStyle( color: Theme.of( context, ).colorScheme.error, ), ), ), message: message, index: index, ), fileMessageBuilder: ( _, message, index, { required bool isSentByMe, MessageGroupStatus? groupStatus, }) => InkWell( onTap: () => showDialog( context: context, builder: (_) => Dialog( child: Text( "TODO: Download Attachments", // TODO ), ), ), child: FlyerChatFileMessage( topWidget: TopWidget( message, headers: room.roomData.client.headers, groupStatus: groupStatus, ), message: message, index: index, ), ), systemMessageBuilder: ( _, message, index, { required bool isSentByMe, MessageGroupStatus? groupStatus, }) => FlyerChatSystemMessage( message: message, index: index, ), unsupportedMessageBuilder: ( _, message, index, { required bool isSentByMe, MessageGroupStatus? groupStatus, }) => Text( "${message.authorId} sent ${message.metadata?["eventType"]}", style: theme.textTheme.labelSmall ?.copyWith(color: Colors.grey), ), ), resolveUser: notifier.resolveUser, chatController: controller, ), ), ), ], ), ), if (memberListOpened.value == true && showMembersByDefault) MemberList(room.roomData), ], ), endDrawer: showMembersByDefault ? null : MemberList(room.roomData), ); }, ); } }