diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 9bca32169..d64f5fbef 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -165,7 +165,9 @@ class ChatEventList extends StatelessWidget { ), highlightMarker: controller.scrollToEventIdMarker == event.eventId, - onSelect: controller.onSelectMessage, + // #Pangea + // onSelect: controller.onSelectMessage, + // Pangea# scrollToEventId: (String eventId) => controller.scrollToEventId(eventId), longPressSelect: controller.selectedEvents.isNotEmpty, diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 9b12f9904..6456ebace 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -7,7 +7,6 @@ import 'package:fluffychat/pages/chat/chat_emoji_picker.dart'; import 'package:fluffychat/pages/chat/chat_event_list.dart'; import 'package:fluffychat/pages/chat/chat_input_row.dart'; import 'package:fluffychat/pages/chat/pinned_events.dart'; -import 'package:fluffychat/pages/chat/reactions_picker.dart'; import 'package:fluffychat/pages/chat/reply_display.dart'; import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart'; import 'package:fluffychat/pangea/choreographer/widgets/start_igc_button.dart'; @@ -35,6 +34,21 @@ class ChatView extends StatelessWidget { const ChatView(this.controller, {super.key}); + // #Pangea + List _editedAppBarActions(BuildContext context) { + if (!controller.selectMode) { + return [ + ChatSettingsPopupMenu( + controller.room, + (!controller.room.isDirectChat && !controller.room.isArchived), + ), + ]; + } else { + return []; + } + } + // Pangea# + List _appBarActions(BuildContext context) { if (controller.selectMode) { return [ @@ -197,26 +211,33 @@ class ChatView extends StatelessWidget { ? null : Theme.of(context).colorScheme.primary, ), - leading: controller.selectMode - ? IconButton( - icon: const Icon(Icons.close), - onPressed: controller.clearSelectedEvents, - tooltip: L10n.of(context)!.close, - color: Theme.of(context).colorScheme.primary, - ) - : UnreadRoomsBadge( - filter: (r) => - r.id != controller.roomId - // #Pangea - && - !r.isAnalyticsRoom, - // Pangea# - badgePosition: BadgePosition.topEnd(end: 8, top: 4), - child: const Center(child: BackButton()), - ), + leading: + // #Pangea + // controller.selectMode + // ? IconButton( + // icon: const Icon(Icons.close), + // onPressed: controller.clearSelectedEvents, + // tooltip: L10n.of(context)!.close, + // color: Theme.of(context).colorScheme.primary, + // ) + // : + // Pangea# + UnreadRoomsBadge( + filter: (r) => + r.id != controller.roomId + // #Pangea + && + !r.isAnalyticsRoom, + // Pangea# + badgePosition: BadgePosition.topEnd(end: 8, top: 4), + child: const Center(child: BackButton()), + ), titleSpacing: 0, title: ChatAppBarTitle(controller), - actions: _appBarActions(context), + // #Pangea + // actions: _appBarActions(context), + actions: _editedAppBarActions(context), + // Pangea# bottom: PreferredSize( preferredSize: Size.fromHeight(appbarBottomHeight), child: Column( @@ -497,7 +518,6 @@ class ChatView extends StatelessWidget { ITBar( choreographer: controller.choreographer, ), - ReactionsPicker(controller), ReplyDisplay(controller), ChatInputRow(controller), ChatEmojiPicker(controller), diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 3a6b7030c..345d74ac1 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -9,7 +9,6 @@ import 'package:fluffychat/utils/string_color.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:swipe_to_action/swipe_to_action.dart'; @@ -26,7 +25,6 @@ class Message extends StatelessWidget { final Event? nextEvent; final Event? previousEvent; final bool displayReadMarker; - final void Function(Event) onSelect; final void Function(Event) onAvatarTab; final void Function(Event) onInfoTab; final void Function(String) scrollToEventId; @@ -38,6 +36,7 @@ class Message extends StatelessWidget { final bool animateIn; final void Function()? resetAnimateIn; // #Pangea + // final void Function(Event) onSelect; final bool immersionMode; final bool definitions; final ChatController controller; @@ -50,7 +49,9 @@ class Message extends StatelessWidget { this.previousEvent, this.displayReadMarker = false, this.longPressSelect = false, - required this.onSelect, + // #Pangea + // required this.onSelect, + // Pangea# required this.onInfoTab, required this.onAvatarTab, required this.scrollToEventId, @@ -203,8 +204,10 @@ class Message extends StatelessWidget { left: 0, right: 0, child: InkWell( - onTap: () => onSelect(event), - onLongPress: () => onSelect(event), + // #Pangea + // onTap: () => onSelect(event), + // onLongPress: () => onSelect(event), + // Pangea# borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2), child: Material( @@ -228,17 +231,20 @@ class Message extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: rowMainAxisAlignment, children: [ - if (longPressSelect) - SizedBox( - height: 32, - width: Avatar.defaultSize, - child: Checkbox.adaptive( - value: selected, - shape: const CircleBorder(), - onChanged: (_) => onSelect(event), - ), - ) - else if (nextEventSameSender || ownMessage) + // #Pangea + // if (longPressSelect) + // SizedBox( + // height: 32, + // width: Avatar.defaultSize, + // child: Checkbox.adaptive( + // value: selected, + // shape: const CircleBorder(), + // onChanged: (_) => onSelect(event), + // ), + // ) + // else + // Pangea# + if (nextEventSameSender || ownMessage) SizedBox( width: Avatar.defaultSize, child: Center( @@ -319,13 +325,13 @@ class Message extends StatelessWidget { ), onDoubleTap: () => toolbarController?.showToolbar(context), + // onLongPress: longPressSelect + // ? null + // : () { + // HapticFeedback.heavyImpact(); + // onSelect(event); + // }, // Pangea# - onLongPress: longPressSelect - ? null - : () { - HapticFeedback.heavyImpact(); - onSelect(event); - }, child: AnimatedOpacity( opacity: animateIn ? 0 diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 5468b59e8..5da553366 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -317,9 +317,11 @@ class MessageContent extends StatelessWidget { pangeaMessageEvent != null && !(toolbarController!.highlighted) && !selected) { - toolbarController!.controller.onSelectMessage( - pangeaMessageEvent!.event, - ); + // #Pangea + // toolbarController!.controller.onSelectMessage( + // pangeaMessageEvent!.event, + // ); + // Pangea# return; } toolbarController?.toolbar?.textSelection diff --git a/lib/pangea/utils/overlay.dart b/lib/pangea/utils/overlay.dart index 44d179b8d..4add2e80a 100644 --- a/lib/pangea/utils/overlay.dart +++ b/lib/pangea/utils/overlay.dart @@ -27,6 +27,7 @@ class OverlayUtil { Alignment? followerAnchor, bool closePrevOverlay = true, bool targetScreen = false, + Function? onDismiss, }) { try { if (closePrevOverlay) { @@ -44,6 +45,7 @@ class OverlayUtil { if (backDropToDismiss) TransparentBackdrop( backgroundColor: backgroundColor, + onDismiss: onDismiss, ), Positioned( width: width, @@ -194,8 +196,10 @@ class OverlayUtil { class TransparentBackdrop extends StatelessWidget { final Color? backgroundColor; + final Function? onDismiss; const TransparentBackdrop({ super.key, + this.onDismiss, this.backgroundColor, }); @@ -211,6 +215,9 @@ class TransparentBackdrop extends StatelessWidget { focusColor: Colors.transparent, highlightColor: Colors.transparent, onTap: () { + if (onDismiss != null) { + onDismiss!(); + } MatrixState.pAnyState.closeOverlay(); }, child: Container( diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index c6f3cd4f2..c798e01f9 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -12,6 +12,8 @@ import 'package:fluffychat/pangea/widgets/chat/message_speech_to_text_card.dart' import 'package:fluffychat/pangea/widgets/chat/message_text_selection.dart'; import 'package:fluffychat/pangea/widgets/chat/message_translation_card.dart'; import 'package:fluffychat/pangea/widgets/chat/message_unsubscribed_card.dart'; +import 'package:fluffychat/pangea/widgets/chat/overlay_footer.dart'; +import 'package:fluffychat/pangea/widgets/chat/overlay_header.dart'; import 'package:fluffychat/pangea/widgets/chat/overlay_message.dart'; import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; @@ -72,7 +74,7 @@ class ToolbarDisplayController { immersionMode: immersionMode, ownMessage: pangeaMessageEvent.ownMessage, toolbarController: this, - width: messageWidth, + width: 300, nextEvent: nextEvent, previousEvent: previousEvent, ); @@ -84,18 +86,33 @@ class ToolbarDisplayController { Widget overlayEntry; if (toolbar == null) return; try { - overlayEntry = Container( - constraints: - BoxConstraints(maxHeight: MediaQuery.sizeOf(context).height * .72), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - toolbar!, - const SizedBox(height: 6), - overlayMessage, - ], - ), + overlayEntry = Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + children: [ + OverlayHeader(controller: controller), + const SizedBox( + height: 7, + ), + Container( + constraints: BoxConstraints( + maxHeight: MediaQuery.sizeOf(context).height * .72, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + toolbar!, + const SizedBox(height: 9), + overlayMessage, + ], + ), + ), + const SizedBox( + height: 7, + ), + OverlayFooter(controller: controller), + ], ); } catch (err) { debugger(when: kDebugMode); @@ -113,9 +130,10 @@ class ToolbarDisplayController { closePrevOverlay: MatrixState.pangeaController.subscriptionController.isSubscribed, targetScreen: true, + onDismiss: controller.clearSelectedEvents, ); - // controller.onSelectMessage(pangeaMessageEvent.event); + controller.onSelectMessage(pangeaMessageEvent.event); if (MatrixState.pAnyState.entries.isNotEmpty) { overlayId = MatrixState.pAnyState.entries.last.hashCode.toString(); diff --git a/lib/pangea/widgets/chat/overlay_footer.dart b/lib/pangea/widgets/chat/overlay_footer.dart new file mode 100644 index 000000000..c886bc54d --- /dev/null +++ b/lib/pangea/widgets/chat/overlay_footer.dart @@ -0,0 +1,57 @@ +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pages/chat/chat_input_row.dart'; +import 'package:fluffychat/pages/chat/reactions_picker.dart'; +import 'package:fluffychat/pages/chat/reply_display.dart'; +import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart'; +import 'package:fluffychat/widgets/connection_status_header.dart'; +import 'package:flutter/material.dart'; + +enum _EventContextAction { info, report } + +class OverlayFooter extends StatelessWidget { + ChatController controller; + + OverlayFooter({ + required this.controller, + super.key, + }); + + @override + Widget build(BuildContext context) { + final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 18.0 : 10.0; + + return Container( + margin: EdgeInsets.only( + bottom: bottomSheetPadding, + left: bottomSheetPadding, + right: bottomSheetPadding, + ), + constraints: const BoxConstraints( + maxWidth: FluffyThemes.columnWidth * 2.5, + ), + alignment: Alignment.center, + child: Material( + clipBehavior: Clip.hardEdge, + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: const BorderRadius.all( + Radius.circular(24), + ), + child: Column( + children: [ + const ConnectionStatusHeader(), + ITBar( + choreographer: controller.choreographer, + ), + ReactionsPicker(controller), + ReplyDisplay(controller), + ChatInputRow(controller), + SizedBox( + height: (FluffyThemes.isColumnMode(context) ? 16.0 : 8.0), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pangea/widgets/chat/overlay_header.dart b/lib/pangea/widgets/chat/overlay_header.dart new file mode 100644 index 000000000..3bac468c1 --- /dev/null +++ b/lib/pangea/widgets/chat/overlay_header.dart @@ -0,0 +1,112 @@ +import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pages/chat/chat_app_bar_title.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + +enum _EventContextAction { info, report } + +class OverlayHeader extends StatelessWidget { + ChatController controller; + + OverlayHeader({ + required this.controller, + super.key, + }); + + @override + Widget build(BuildContext context) { + final Event selectedEvent = controller.selectedEvents.single; + + return AppBar( + actionsIconTheme: IconThemeData( + color: Theme.of(context).colorScheme.primary, + ), + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: MatrixState.pAnyState.closeOverlay, + tooltip: L10n.of(context)!.close, + color: Theme.of(context).colorScheme.primary, + ), + titleSpacing: 0, + title: ChatAppBarTitle(controller), + actions: [ + if (controller.canEditSelectedEvents) + IconButton( + icon: const Icon(Icons.edit_outlined), + tooltip: L10n.of(context)!.edit, + onPressed: controller.editSelectedEventAction, + ), + if (selectedEvent.messageType == MessageTypes.Text) + IconButton( + icon: const Icon(Icons.copy_outlined), + tooltip: L10n.of(context)!.copy, + onPressed: controller.copyEventsAction, + ), + if (controller.canSaveSelectedEvent) + // Use builder context to correctly position the share dialog on iPad + Builder( + builder: (context) => IconButton( + icon: Icon(Icons.adaptive.share), + tooltip: L10n.of(context)!.share, + onPressed: () => controller.saveSelectedEvent(context), + ), + ), + if (controller.canPinSelectedEvents) + IconButton( + icon: const Icon(Icons.push_pin_outlined), + onPressed: controller.pinEvent, + tooltip: L10n.of(context)!.pinMessage, + ), + if (controller.canRedactSelectedEvents) + IconButton( + icon: const Icon(Icons.delete_outlined), + tooltip: L10n.of(context)!.redactMessage, + onPressed: controller.redactEventsAction, + ), + PopupMenuButton<_EventContextAction>( + onSelected: (action) { + switch (action) { + case _EventContextAction.info: + controller.showEventInfo(); + controller.clearSelectedEvents(); + break; + case _EventContextAction.report: + controller.reportEventAction(); + break; + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: _EventContextAction.info, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.info_outlined), + const SizedBox(width: 12), + Text(L10n.of(context)!.messageInfo), + ], + ), + ), + if (selectedEvent.status.isSent) + PopupMenuItem( + value: _EventContextAction.report, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.shield_outlined, + color: Colors.red, + ), + const SizedBox(width: 12), + Text(L10n.of(context)!.reportMessage), + ], + ), + ), + ], + ), + ], + ); + } +} diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index abf583b3b..7ab5ba61e 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -143,9 +143,9 @@ class PangeaRichTextState extends State { (e) => e.eventId == widget.pangeaMessageEvent.eventId, ) ?? false)) { - widget.toolbarController?.controller.onSelectMessage( - widget.pangeaMessageEvent.event, - ); + // widget.toolbarController?.controller.onSelectMessage( + // widget.pangeaMessageEvent.event, + // ); return; } widget.toolbarController?.toolbar?.textSelection