simplified positioning of toolbar

pull/1384/head
ggurdin 1 year ago
parent 868e83709d
commit 0373d01f1b
No known key found for this signature in database
GPG Key ID: A01CB41737CBB478

@ -3,14 +3,12 @@ import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:collection/collection.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/setting_keys.dart';
import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/chat_emoji_picker.dart';
import 'package:fluffychat/pages/chat/chat_view.dart'; import 'package:fluffychat/pages/chat/chat_view.dart';
import 'package:fluffychat/pages/chat/event_info_dialog.dart'; import 'package:fluffychat/pages/chat/event_info_dialog.dart';
import 'package:fluffychat/pages/chat/recording_dialog.dart'; import 'package:fluffychat/pages/chat/recording_dialog.dart';
@ -18,6 +16,7 @@ import 'package:fluffychat/pages/chat_details/chat_details.dart';
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/models/choreo_record.dart'; import 'package:fluffychat/pangea/models/choreo_record.dart';
@ -29,8 +28,10 @@ import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
import 'package:fluffychat/pangea/utils/overlay.dart'; import 'package:fluffychat/pangea/utils/overlay.dart';
import 'package:fluffychat/pangea/utils/report_message.dart'; import 'package:fluffychat/pangea/utils/report_message.dart';
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
import 'package:fluffychat/pangea/widgets/chat/message_text_selection.dart';
import 'package:fluffychat/pangea/widgets/igc/pangea_text_controller.dart'; import 'package:fluffychat/pangea/widgets/igc/pangea_text_controller.dart';
import 'package:fluffychat/pangea/widgets/user_settings/p_language_dialog.dart';
import 'package:fluffychat/utils/error_reporter.dart'; import 'package:fluffychat/utils/error_reporter.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart';
@ -928,20 +929,17 @@ class ChatController extends State<ChatPageWithRoom>
} }
void copyEventsAction() { void copyEventsAction() {
// #Pangea
MatrixState.pAnyState.closeAllOverlays();
// Pangea#
Clipboard.setData(ClipboardData(text: _getSelectedEventString())); Clipboard.setData(ClipboardData(text: _getSelectedEventString()));
setState(() { setState(() {
showEmojiPicker = false; showEmojiPicker = false;
selectedEvents.clear(); // #Pangea
// selectedEvents.clear();
clearSelectedEvents();
// Pangea#
}); });
} }
void reportEventAction() async { void reportEventAction() async {
// #Pangea
MatrixState.pAnyState.closeAllOverlays();
// Pangea#
final event = selectedEvents.single; final event = selectedEvents.single;
// #Pangea // #Pangea
clearSelectedEvents(); clearSelectedEvents();
@ -1035,9 +1033,6 @@ class ChatController extends State<ChatPageWithRoom>
} }
void redactEventsAction() async { void redactEventsAction() async {
// #Pangea
MatrixState.pAnyState.closeAllOverlays();
// Pangea#
final reasonInput = selectedEvents.any((event) => event.status.isSent) final reasonInput = selectedEvents.any((event) => event.status.isSent)
? await showTextInputDialog( ? await showTextInputDialog(
context: context, context: context,
@ -1086,6 +1081,9 @@ class ChatController extends State<ChatPageWithRoom>
}, },
); );
} }
// #Pangea
clearSelectedEvents();
// Pangea#
setState(() { setState(() {
showEmojiPicker = false; showEmojiPicker = false;
selectedEvents.clear(); selectedEvents.clear();
@ -1133,9 +1131,6 @@ class ChatController extends State<ChatPageWithRoom>
} }
void forwardEventsAction() async { void forwardEventsAction() async {
// #Pangea
MatrixState.pAnyState.closeAllOverlays();
// Pangea#
if (selectedEvents.length == 1) { if (selectedEvents.length == 1) {
Matrix.of(context).shareContent = Matrix.of(context).shareContent =
selectedEvents.first.getDisplayEvent(timeline!).content; selectedEvents.first.getDisplayEvent(timeline!).content;
@ -1169,7 +1164,7 @@ class ChatController extends State<ChatPageWithRoom>
selectedEvents.clear(); selectedEvents.clear();
}); });
// #Pangea // #Pangea
MatrixState.pAnyState.closeAllOverlays(); clearSelectedEvents();
// Pangea // Pangea
inputFocus.requestFocus(); inputFocus.requestFocus();
} }
@ -1283,39 +1278,32 @@ class ChatController extends State<ChatPageWithRoom>
} }
void pickEmojiReactionAction(Iterable<Event> allReactionEvents) async { void pickEmojiReactionAction(Iterable<Event> allReactionEvents) async {
// #Pangea
MatrixState.pAnyState.closeAllOverlays();
// Pangea#
_allReactionEvents = allReactionEvents; _allReactionEvents = allReactionEvents;
emojiPickerType = EmojiPickerType.reaction; emojiPickerType = EmojiPickerType.reaction;
setState(() => showEmojiPicker = true); setState(() => showEmojiPicker = true);
// #Pangea
OverlayUtil.showOverlay(
context: context,
child: ChatEmojiPicker(this),
transformTargetId: selectedEvents.first.eventId,
targetAnchor: Alignment.center,
followerAnchor: Alignment.center,
backgroundColor: const Color.fromRGBO(0, 0, 0, 1).withAlpha(100),
closePrevOverlay: false,
onDismiss: hideEmojiPicker,
position: OverlayEnum.bottom,
);
// Pangea#
} }
void sendEmojiAction(String? emoji) async { void sendEmojiAction(String? emoji) async {
final events = List<Event>.from(selectedEvents); final events = List<Event>.from(selectedEvents);
setState(() => selectedEvents.clear()); setState(() => selectedEvents.clear());
// #Pangea
MatrixState.pAnyState.closeAllOverlays();
// Pangea#
for (final event in events) { for (final event in events) {
await room.sendReaction( await room.sendReaction(
event.eventId, event.eventId,
emoji!, emoji!,
); );
} }
// #Pangea
clearSelectedEvents();
// Pangea#
} }
void clearSelectedEvents() => setState(() { void clearSelectedEvents() => setState(() {
// #Pangea
MatrixState.pAnyState.closeAllOverlays();
// Pangea#
selectedEvents.clear(); selectedEvents.clear();
showEmojiPicker = false; showEmojiPicker = false;
}); });
@ -1552,12 +1540,7 @@ class ChatController extends State<ChatPageWithRoom>
bool get isArchived => bool get isArchived =>
{Membership.leave, Membership.ban}.contains(room.membership); {Membership.leave, Membership.ban}.contains(room.membership);
void showEventInfo([Event? event]) void showEventInfo([Event? event]) {
// #Pangea
// =>
{
MatrixState.pAnyState.closeAllOverlays();
// Pangea#
(event ?? selectedEvents.single).showInfoDialog(context); (event ?? selectedEvents.single).showInfoDialog(context);
// #Pangea // #Pangea
clearSelectedEvents(); clearSelectedEvents();
@ -1618,80 +1601,51 @@ class ChatController extends State<ChatPageWithRoom>
editEvent = null; editEvent = null;
}); });
// #Pangea // #Pangea
final Map<String, PangeaMessageEvent> _pangeaMessageEvents = {}; MessageTextSelection textSelection = MessageTextSelection();
final Map<String, ToolbarDisplayController> _toolbarDisplayControllers = {};
void setPangeaMessageEvent(String eventId) {
final Event? event = timeline!.events.firstWhereOrNull(
(e) => e.eventId == eventId,
);
if (event == null || timeline == null) return;
_pangeaMessageEvents[eventId] = PangeaMessageEvent(
event: event,
timeline: timeline!,
ownMessage: event.senderId == room.client.userID,
);
}
void setToolbarDisplayController( void showToolbar(
String eventId, { PangeaMessageEvent pangeaMessageEvent, {
Event? nextEvent, MessageMode? mode,
Event? previousEvent,
}) { }) {
final Event? event = timeline!.events.firstWhereOrNull( // Close keyboard, if open
(e) => e.eventId == eventId, if (inputFocus.hasFocus && PlatformInfos.isMobile) {
); inputFocus.unfocus();
if (event == null || timeline == null) return; return;
if (_pangeaMessageEvents[eventId] == null) {
setPangeaMessageEvent(eventId);
if (_pangeaMessageEvents[eventId] == null) return;
} }
// Close emoji picker, if open
showEmojiPicker = false;
// Check if the user has set their languages. If not, prompt them to do so.
if (!MatrixState.pangeaController.languageController.languagesSet) {
pLanguageDialog(context, () {});
return;
}
Widget? overlayEntry;
try { try {
_toolbarDisplayControllers[eventId] = ToolbarDisplayController( overlayEntry = MessageSelectionOverlay(
targetId: event.eventId,
pangeaMessageEvent: _pangeaMessageEvents[eventId]!,
immersionMode: choreographer.immersionMode,
controller: this, controller: this,
nextEvent: nextEvent, event: pangeaMessageEvent.event,
previousEvent: previousEvent, pangeaMessageEvent: pangeaMessageEvent,
); textSelection: textSelection,
_toolbarDisplayControllers[eventId]!.setToolbar();
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
m: "Failed to set toolbar display controller",
data: {
"eventId": eventId,
"event": event.toJson(),
"pangeaMessageEvent": _pangeaMessageEvents[eventId]?.toString(),
},
); );
} catch (err) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: err, s: StackTrace.current);
return;
} }
}
PangeaMessageEvent? getPangeaMessageEvent(String eventId) {
if (_pangeaMessageEvents[eventId] == null) {
setPangeaMessageEvent(eventId);
}
return _pangeaMessageEvents[eventId];
}
ToolbarDisplayController? getToolbarDisplayController( OverlayUtil.showOverlay(
String eventId, { context: context,
Event? nextEvent, child: overlayEntry,
Event? previousEvent, transformTargetId: "",
}) { backgroundColor: const Color.fromRGBO(0, 0, 0, 1).withAlpha(200),
if (_toolbarDisplayControllers[eventId] == null) { closePrevOverlay:
setToolbarDisplayController( MatrixState.pangeaController.subscriptionController.isSubscribed,
eventId, position: OverlayEnum.centered,
nextEvent: nextEvent, onDismiss: clearSelectedEvents,
previousEvent: previousEvent, );
);
}
return _toolbarDisplayControllers[eventId];
} }
// Pangea# // Pangea#

@ -14,90 +14,95 @@ class ChatEmojiPicker extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
return AnimatedContainer( // #Pangea
duration: FluffyThemes.animationDuration, return Material(
curve: FluffyThemes.animationCurve, // Pangea#
clipBehavior: Clip.hardEdge, child: AnimatedContainer(
decoration: const BoxDecoration(), duration: FluffyThemes.animationDuration,
height: controller.showEmojiPicker curve: FluffyThemes.animationCurve,
? MediaQuery.of(context).size.height / 2 clipBehavior: Clip.hardEdge,
: 0, decoration: const BoxDecoration(),
child: controller.showEmojiPicker height: controller.showEmojiPicker
? DefaultTabController( ? MediaQuery.of(context).size.height / 2
length: 2, : 0,
child: Column( child: controller.showEmojiPicker
children: [ ? DefaultTabController(
TabBar( length: 2,
tabs: [ child: Column(
Tab(text: L10n.of(context)!.emojis), children: [
Tab(text: L10n.of(context)!.stickers), TabBar(
], tabs: [
), Tab(text: L10n.of(context)!.emojis),
Expanded( Tab(text: L10n.of(context)!.stickers),
child: TabBarView( ],
children: [ ),
EmojiPicker( Expanded(
onEmojiSelected: controller.onEmojiSelected, child: TabBarView(
onBackspacePressed: controller.emojiPickerBackspace, children: [
config: Config( EmojiPicker(
emojiViewConfig: EmojiViewConfig( onEmojiSelected: controller.onEmojiSelected,
noRecents: const NoRecent(), onBackspacePressed: controller.emojiPickerBackspace,
backgroundColor: Theme.of(context) config: Config(
.colorScheme emojiViewConfig: EmojiViewConfig(
.onInverseSurface, noRecents: const NoRecent(),
), backgroundColor: Theme.of(context)
bottomActionBarConfig: const BottomActionBarConfig( .colorScheme
enabled: false, .onInverseSurface,
), ),
categoryViewConfig: CategoryViewConfig( bottomActionBarConfig:
backspaceColor: theme.colorScheme.primary, const BottomActionBarConfig(
iconColor: enabled: false,
theme.colorScheme.primary.withOpacity(0.5), ),
iconColorSelected: theme.colorScheme.primary, categoryViewConfig: CategoryViewConfig(
indicatorColor: theme.colorScheme.primary, backspaceColor: theme.colorScheme.primary,
), iconColor:
skinToneConfig: SkinToneConfig( theme.colorScheme.primary.withOpacity(0.5),
dialogBackgroundColor: Color.lerp( iconColorSelected: theme.colorScheme.primary,
theme.colorScheme.surface, indicatorColor: theme.colorScheme.primary,
theme.colorScheme.primaryContainer, ),
0.75, skinToneConfig: SkinToneConfig(
)!, dialogBackgroundColor: Color.lerp(
indicatorColor: theme.colorScheme.onSurface, theme.colorScheme.surface,
theme.colorScheme.primaryContainer,
0.75,
)!,
indicatorColor: theme.colorScheme.onSurface,
),
), ),
), ),
), StickerPickerDialog(
StickerPickerDialog( room: controller.room,
room: controller.room, onSelected: (sticker) {
onSelected: (sticker) { controller.room.sendEvent(
controller.room.sendEvent( {
{ 'body': sticker.body,
'body': sticker.body, 'info': sticker.info ?? {},
'info': sticker.info ?? {}, 'url': sticker.url.toString(),
'url': sticker.url.toString(), },
}, type: EventTypes.Sticker,
type: EventTypes.Sticker, );
); controller.hideEmojiPicker();
controller.hideEmojiPicker(); },
}, ),
), ],
], ),
), ),
), // #Pangea
// #Pangea Padding(
Padding( padding: const EdgeInsets.symmetric(vertical: 8.0),
padding: const EdgeInsets.symmetric(vertical: 8.0), child: FloatingActionButton(
child: FloatingActionButton( onPressed: controller.hideEmojiPicker,
onPressed: controller.hideEmojiPicker, shape: const CircleBorder(),
shape: const CircleBorder(), mini: true,
mini: true, child: const Icon(Icons.close),
child: const Icon(Icons.close), ),
), ),
), // Pangea#
// Pangea# ],
], ),
), )
) : null,
: null, ),
); );
} }
} }

@ -188,7 +188,6 @@ class ChatEventList extends StatelessWidget {
longPressSelect: controller.selectedEvents.isNotEmpty, longPressSelect: controller.selectedEvents.isNotEmpty,
// #Pangea // #Pangea
immersionMode: controller.choreographer.immersionMode, immersionMode: controller.choreographer.immersionMode,
definitions: controller.choreographer.definitionsEnabled,
controller: controller, controller: controller,
// Pangea# // Pangea#
selected: controller.selectedEvents selected: controller.selectedEvents

@ -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_event_list.dart';
import 'package:fluffychat/pages/chat/chat_input_row.dart'; import 'package:fluffychat/pages/chat/chat_input_row.dart';
import 'package:fluffychat/pages/chat/pinned_events.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/pages/chat/reply_display.dart';
import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart'; import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart';
import 'package:fluffychat/pangea/choreographer/widgets/start_igc_button.dart'; import 'package:fluffychat/pangea/choreographer/widgets/start_igc_button.dart';
@ -36,100 +35,98 @@ class ChatView extends StatelessWidget {
const ChatView(this.controller, {super.key}); const ChatView(this.controller, {super.key});
List<Widget> _appBarActions(BuildContext context) { List<Widget> _appBarActions(BuildContext context) {
if (controller.selectMode) { // #Pangea
return [ // if (controller.selectMode) {
if (controller.canEditSelectedEvents) // return [
IconButton( // if (controller.canEditSelectedEvents)
icon: const Icon(Icons.edit_outlined), // IconButton(
tooltip: L10n.of(context)!.edit, // icon: const Icon(Icons.edit_outlined),
onPressed: controller.editSelectedEventAction, // tooltip: L10n.of(context)!.edit,
), // onPressed: controller.editSelectedEventAction,
// #Pangea // ),
if (controller.selectedEvents.length == 1 && // // #Pangea
controller.selectedEvents.single.messageType == MessageTypes.Text) // if (controller.selectedEvents.length == 1 &&
// Pangea# // controller.selectedEvents.single.messageType == MessageTypes.Text)
IconButton( // // Pangea#
icon: const Icon(Icons.copy_outlined), // IconButton(
tooltip: L10n.of(context)!.copy, // icon: const Icon(Icons.copy_outlined),
onPressed: controller.copyEventsAction, // tooltip: L10n.of(context)!.copy,
), // onPressed: controller.copyEventsAction,
if (controller.canSaveSelectedEvent) // ),
// Use builder context to correctly position the share dialog on iPad // if (controller.canSaveSelectedEvent)
Builder( // // Use builder context to correctly position the share dialog on iPad
builder: (context) => IconButton( // Builder(
icon: Icon(Icons.adaptive.share), // builder: (context) => IconButton(
tooltip: L10n.of(context)!.share, // icon: Icon(Icons.adaptive.share),
onPressed: () => controller.saveSelectedEvent(context), // tooltip: L10n.of(context)!.share,
), // onPressed: () => controller.saveSelectedEvent(context),
), // ),
if (controller.canPinSelectedEvents) // ),
IconButton( // if (controller.canPinSelectedEvents)
icon: const Icon(Icons.push_pin_outlined), // IconButton(
onPressed: controller.pinEvent, // icon: const Icon(Icons.push_pin_outlined),
tooltip: L10n.of(context)!.pinMessage, // onPressed: controller.pinEvent,
), // tooltip: L10n.of(context)!.pinMessage,
if (controller.canRedactSelectedEvents) // ),
IconButton( // if (controller.canRedactSelectedEvents)
icon: const Icon(Icons.delete_outlined), // IconButton(
tooltip: L10n.of(context)!.redactMessage, // icon: const Icon(Icons.delete_outlined),
onPressed: controller.redactEventsAction, // tooltip: L10n.of(context)!.redactMessage,
), // onPressed: controller.redactEventsAction,
if (controller.selectedEvents.length == 1) // ),
PopupMenuButton<_EventContextAction>( // if (controller.selectedEvents.length == 1)
onSelected: (action) { // PopupMenuButton<_EventContextAction>(
switch (action) { // onSelected: (action) {
case _EventContextAction.info: // switch (action) {
controller.showEventInfo(); // case _EventContextAction.info:
controller.clearSelectedEvents(); // controller.showEventInfo();
break; // controller.clearSelectedEvents();
case _EventContextAction.report: // break;
controller.reportEventAction(); // case _EventContextAction.report:
break; // controller.reportEventAction();
} // break;
}, // }
itemBuilder: (context) => [ // },
PopupMenuItem( // itemBuilder: (context) => [
value: _EventContextAction.info, // PopupMenuItem(
child: Row( // value: _EventContextAction.info,
mainAxisSize: MainAxisSize.min, // child: Row(
children: [ // mainAxisSize: MainAxisSize.min,
const Icon(Icons.info_outlined), // children: [
const SizedBox(width: 12), // const Icon(Icons.info_outlined),
Text(L10n.of(context)!.messageInfo), // const SizedBox(width: 12),
], // Text(L10n.of(context)!.messageInfo),
), // ],
), // ),
if (controller.selectedEvents.single.status.isSent) // ),
PopupMenuItem( // if (controller.selectedEvents.single.status.isSent)
value: _EventContextAction.report, // PopupMenuItem(
child: Row( // value: _EventContextAction.report,
mainAxisSize: MainAxisSize.min, // child: Row(
children: [ // mainAxisSize: MainAxisSize.min,
const Icon( // children: [
Icons.shield_outlined, // const Icon(
color: Colors.red, // Icons.shield_outlined,
), // color: Colors.red,
const SizedBox(width: 12), // ),
Text(L10n.of(context)!.reportMessage), // const SizedBox(width: 12),
], // Text(L10n.of(context)!.reportMessage),
), // ],
), // ),
], // ),
), // ],
]; // ),
// #Pangea // ];
} else { return [
return [ RoundTimer(controller: controller),
RoundTimer(controller: controller), const SizedBox(
const SizedBox( width: 10,
width: 10, ),
), ChatSettingsPopupMenu(
ChatSettingsPopupMenu( controller.room,
controller.room, (!controller.room.isDirectChat && !controller.room.isArchived),
(!controller.room.isDirectChat && !controller.room.isArchived), ),
), ];
];
}
// else if (!controller.room.isArchived) { // else if (!controller.room.isArchived) {
// return [ // return [
// if (Matrix.of(context).voipPlugin != null && // if (Matrix.of(context).voipPlugin != null &&
@ -196,28 +193,34 @@ class ChatView extends StatelessWidget {
} }
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
actionsIconTheme: IconThemeData( actionsIconTheme: const IconThemeData(
color: controller.selectedEvents.isEmpty // #Pangea
? null // color: controller.selectedEvents.isEmpty
: Theme.of(context).colorScheme.primary, // ? null
// : Theme.of(context).colorScheme.primary,
// Pangea#
),
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()),
), ),
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()),
),
titleSpacing: 0, titleSpacing: 0,
title: ChatAppBarTitle(controller), title: ChatAppBarTitle(controller),
actions: _appBarActions(context), actions: _appBarActions(context),
@ -501,7 +504,6 @@ class ChatView extends StatelessWidget {
ITBar( ITBar(
choreographer: controller.choreographer, choreographer: controller.choreographer,
), ),
ReactionsPicker(controller),
ReplyDisplay(controller), ReplyDisplay(controller),
ChatInputRow(controller), ChatInputRow(controller),
ChatEmojiPicker(controller), ChatEmojiPicker(controller),

@ -2,13 +2,14 @@ import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/enum/use_type.dart'; import 'package:fluffychat/pangea/enum/use_type.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/utils/any_state_holder.dart';
import 'package:fluffychat/pangea/widgets/chat/message_buttons.dart'; import 'package:fluffychat/pangea/widgets/chat/message_buttons.dart';
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/utils/string_color.dart'; import 'package:fluffychat/utils/string_color.dart';
import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:swipe_to_action/swipe_to_action.dart'; import 'package:swipe_to_action/swipe_to_action.dart';
@ -38,8 +39,8 @@ class Message extends StatelessWidget {
// #Pangea // #Pangea
// final void Function(Event) onSelect; // final void Function(Event) onSelect;
final bool immersionMode; final bool immersionMode;
final bool definitions;
final ChatController controller; final ChatController controller;
final bool isOverlay;
// Pangea# // Pangea#
final Color? avatarPresenceBackgroundColor; final Color? avatarPresenceBackgroundColor;
@ -64,21 +65,33 @@ class Message extends StatelessWidget {
this.avatarPresenceBackgroundColor, this.avatarPresenceBackgroundColor,
// #Pangea // #Pangea
required this.immersionMode, required this.immersionMode,
required this.definitions,
required this.controller, required this.controller,
this.isOverlay = false,
// Pangea# // Pangea#
super.key, super.key,
}); });
// #Pangea // #Pangea
PangeaMessageEvent? get pangeaMessageEvent => void showToolbar(PangeaMessageEvent? pangeaMessageEvent) {
controller.getPangeaMessageEvent(event.eventId); if (pangeaMessageEvent != null && !isOverlay) {
HapticFeedback.mediumImpact();
controller.showToolbar(pangeaMessageEvent);
}
}
// Pangea# // Pangea#
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// #Pangea // #Pangea
debugPrint('Message.build()'); debugPrint('Message.build()');
PangeaMessageEvent? pangeaMessageEvent;
if (event.type == EventTypes.Message) {
pangeaMessageEvent = PangeaMessageEvent(
event: event,
timeline: timeline,
ownMessage: event.senderId == Matrix.of(context).client.userID,
);
}
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (controller.pangeaEditingEvent?.eventId == event.eventId) { if (controller.pangeaEditingEvent?.eventId == event.eventId) {
pangeaMessageEvent?.updateLatestEdit(); pangeaMessageEvent?.updateLatestEdit();
@ -109,8 +122,13 @@ class Message extends StatelessWidget {
// ignore: deprecated_member_use // ignore: deprecated_member_use
var color = Theme.of(context).colorScheme.surfaceVariant; var color = Theme.of(context).colorScheme.surfaceVariant;
final displayTime = event.type == EventTypes.RoomCreate || final displayTime = event.type == EventTypes.RoomCreate ||
nextEvent == null || nextEvent == null ||
!event.originServerTs.sameEnvironment(nextEvent!.originServerTs); !event.originServerTs.sameEnvironment(nextEvent!.originServerTs)
// #Pangea
&&
!isOverlay
// Pangea#
;
final nextEventSameSender = nextEvent != null && final nextEventSameSender = nextEvent != null &&
{ {
EventTypes.Message, EventTypes.Message,
@ -163,370 +181,405 @@ class Message extends StatelessWidget {
: Theme.of(context).colorScheme.primary; : Theme.of(context).colorScheme.primary;
} }
// #Pangea
ToolbarDisplayController? toolbarController;
if (event.type == EventTypes.Message &&
!event.redacted &&
(event.messageType == MessageTypes.Text ||
event.messageType == MessageTypes.Notice ||
event.messageType == MessageTypes.Audio)) {
toolbarController = controller.getToolbarDisplayController(
event.eventId,
nextEvent: nextEvent,
previousEvent: previousEvent,
);
}
// Pangea#
final resetAnimateIn = this.resetAnimateIn; final resetAnimateIn = this.resetAnimateIn;
var animateIn = this.animateIn; var animateIn = this.animateIn;
final row = StatefulBuilder( final row =
builder: (context, setState) { // #Pangea
if (animateIn && resetAnimateIn != null) { Material(
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { color: Colors.transparent,
animateIn = false; child:
setState(resetAnimateIn); // Pangea#
}); StatefulBuilder(
} builder: (context, setState) {
return AnimatedSize( if (animateIn && resetAnimateIn != null) {
duration: FluffyThemes.animationDuration, WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
curve: FluffyThemes.animationCurve, animateIn = false;
clipBehavior: Clip.none, setState(resetAnimateIn);
alignment: ownMessage ? Alignment.bottomRight : Alignment.bottomLeft, });
child: animateIn }
? const SizedBox(height: 0, width: double.infinity) return AnimatedSize(
: Stack( duration: FluffyThemes.animationDuration,
children: [ curve: FluffyThemes.animationCurve,
Positioned( clipBehavior: Clip.none,
top: 0, alignment:
bottom: 0, ownMessage ? Alignment.bottomRight : Alignment.bottomLeft,
left: 0, child: animateIn
right: 0, ? const SizedBox(height: 0, width: double.infinity)
child: InkWell( : Stack(
// #Pangea children: [
// onTap: () => onSelect(event), Positioned(
// onLongPress: () => onSelect(event), top: 0,
// Pangea# bottom: 0,
borderRadius: left: 0,
BorderRadius.circular(AppConfig.borderRadius / 2), right: 0,
child: Material( child: InkWell(
// #Pangea
onTap: () => MatrixState.pAnyState.closeOverlay(),
// onTap: () => onSelect(event),
// onLongPress: () => onSelect(event),
// Pangea#
borderRadius: borderRadius:
BorderRadius.circular(AppConfig.borderRadius / 2), BorderRadius.circular(AppConfig.borderRadius / 2),
color: selected child: Material(
? Theme.of(context) borderRadius: BorderRadius.circular(
.colorScheme AppConfig.borderRadius / 2,
.secondaryContainer ),
.withAlpha(100) color: selected
: highlightMarker ? Theme.of(context)
? Theme.of(context) .colorScheme
.colorScheme .secondaryContainer
.tertiaryContainer .withAlpha(100)
.withAlpha(100) : highlightMarker
: Colors.transparent, ? Theme.of(context)
.colorScheme
.tertiaryContainer
.withAlpha(100)
: Colors.transparent,
),
), ),
), ),
), Row(
Row( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: rowMainAxisAlignment,
mainAxisAlignment: rowMainAxisAlignment, children: [
children: [ // #Pangea
// #Pangea // if (longPressSelect)
// if (longPressSelect) // SizedBox(
// SizedBox( // height: 32,
// height: 32, // width: Avatar.defaultSize,
// width: Avatar.defaultSize, // child: Checkbox.adaptive(
// child: Checkbox.adaptive( // value: selected,
// value: selected, // shape: const CircleBorder(),
// shape: const CircleBorder(), // onChanged: (_) => onSelect(event),
// onChanged: (_) => onSelect(event), // ),
// ), // )
// ) // else
// else // Pangea#
// Pangea# if (nextEventSameSender ||
if (nextEventSameSender || ownMessage) ownMessage
SizedBox( // #Pangea
width: Avatar.defaultSize, ||
child: Center( isOverlay
child: SizedBox( // Pangea#
width: 16, )
height: 16, SizedBox(
child: event.status == EventStatus.error width: Avatar.defaultSize,
? const Icon(Icons.error, color: Colors.red) child: Center(
: event.fileSendingStatus != null child: SizedBox(
? const CircularProgressIndicator width: 16,
.adaptive( height: 16,
strokeWidth: 1, child: event.status == EventStatus.error
) ? const Icon(
: null, Icons.error,
color: Colors.red,
)
: event.fileSendingStatus != null
? const CircularProgressIndicator
.adaptive(
strokeWidth: 1,
)
: null,
),
), ),
)
else
FutureBuilder<User?>(
future: event.fetchSenderUser(),
builder: (context, snapshot) {
final user = snapshot.data ??
event.senderFromMemoryOrFallback;
return Avatar(
// mxContent: user.avatarUrl,
// name: user.calcDisplayname(),
// presenceUserId: user.stateKey,
name: "?",
presenceBackgroundColor:
avatarPresenceBackgroundColor,
onTap: () => onAvatarTab(event),
);
},
), ),
) Expanded(
else child: Column(
FutureBuilder<User?>( crossAxisAlignment: CrossAxisAlignment.start,
future: event.fetchSenderUser(), mainAxisSize: MainAxisSize.min,
builder: (context, snapshot) { children: [
final user = snapshot.data ?? if (!nextEventSameSender
event.senderFromMemoryOrFallback; // #Pangea
return Avatar( &&
// mxContent: user.avatarUrl, !isOverlay
// name: user.calcDisplayname(), // Pangea#
// presenceUserId: user.stateKey, )
name: "?", Padding(
presenceBackgroundColor: padding: const EdgeInsets.only(
avatarPresenceBackgroundColor, left: 8.0,
onTap: () => onAvatarTab(event), bottom: 4,
); ),
}, child: ownMessage || event.room.isDirectChat
), ? const SizedBox(height: 12)
Expanded( : FutureBuilder<User?>(
child: Column( future: event.fetchSenderUser(),
crossAxisAlignment: CrossAxisAlignment.start, builder: (context, snapshot) {
mainAxisSize: MainAxisSize.min, // final displayname = snapshot.data
children: [ // ?.calcDisplayname() ??
if (!nextEventSameSender) // event.senderFromMemoryOrFallback
Padding( // .calcDisplayname();
padding: const EdgeInsets.only( const displayname = "?";
left: 8.0, return Text(
bottom: 4, displayname,
style: TextStyle(
fontSize: 12,
color: (Theme.of(context)
.brightness ==
Brightness.light
? displayname.color
: displayname
.lightColorText),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
},
),
), ),
child: ownMessage || event.room.isDirectChat Container(
? const SizedBox(height: 12) alignment: alignment,
: FutureBuilder<User?>( padding: const EdgeInsets.only(left: 8),
future: event.fetchSenderUser(), child: GestureDetector(
builder: (context, snapshot) { // #Pangea
// final displayname = snapshot.data onTap: () =>
// ?.calcDisplayname() ?? showToolbar(pangeaMessageEvent),
// event.senderFromMemoryOrFallback onDoubleTap: () =>
// .calcDisplayname(); showToolbar(pangeaMessageEvent),
const displayname = "?"; onLongPress: () =>
return Text( showToolbar(pangeaMessageEvent),
displayname, // onLongPress: longPressSelect
style: TextStyle( // ? null
fontSize: 12, // : () {
color: (Theme.of(context) // HapticFeedback.heavyImpact();
.brightness == // onSelect(event);
Brightness.light // },
? displayname.color // Pangea#
: displayname child: AnimatedOpacity(
.lightColorText), opacity: animateIn
), ? 0
maxLines: 1, : event.redacted ||
overflow: TextOverflow.ellipsis, event.messageType ==
); MessageTypes
}, .BadEncrypted ||
event.status.isSending
? 0.5
: 1,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
child: Material(
color: noBubble
? Colors.transparent
: color,
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: borderRadius,
), ),
), // #Pangea
Container( child: CompositedTransformTarget(
alignment: alignment, link: isOverlay
padding: const EdgeInsets.only(left: 8), ? LayerLinkAndKey('overlay_msg')
child: GestureDetector( .link
// #Pangea : MatrixState.pAnyState
onTap: () => toolbarController?.showToolbar( .layerLinkAndKey(
context, event.eventId,
), )
onDoubleTap: () => .link,
toolbarController?.showToolbar(context), child: Container(
// onLongPress: longPressSelect key: isOverlay
// ? null ? LayerLinkAndKey('overlay_msg')
// : () { .key
// HapticFeedback.heavyImpact(); : MatrixState.pAnyState
// onSelect(event); .layerLinkAndKey(
// }, event.eventId,
// Pangea# )
child: AnimatedOpacity( .key,
opacity: animateIn // Pangea#
? 0 decoration: BoxDecoration(
: event.redacted || borderRadius:
event.messageType == BorderRadius.circular(
MessageTypes.BadEncrypted || AppConfig.borderRadius,
event.status.isSending ),
? 0.5
: 1,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
child: Material(
color:
noBubble ? Colors.transparent : color,
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: borderRadius,
),
// #Pangea
child: CompositedTransformTarget(
link: MatrixState.pAnyState
.layerLinkAndKey(event.eventId)
.link,
child: Container(
key: MatrixState.pAnyState
.layerLinkAndKey(event.eventId)
.key,
// Pangea#
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
), ),
), padding: noBubble || noPadding
padding: noBubble || noPadding ? EdgeInsets.zero
? EdgeInsets.zero : const EdgeInsets.symmetric(
: const EdgeInsets.symmetric( horizontal: 16,
horizontal: 16, vertical: 8,
vertical: 8, ),
), constraints: const BoxConstraints(
constraints: const BoxConstraints( maxWidth:
maxWidth: FluffyThemes.columnWidth *
FluffyThemes.columnWidth * 1.5, 1.5,
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: crossAxisAlignment:
CrossAxisAlignment.start, CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
if (event.relationshipType == if (event.relationshipType ==
RelationshipTypes.reply) RelationshipTypes.reply)
FutureBuilder<Event?>( FutureBuilder<Event?>(
future: event future: event.getReplyEvent(
.getReplyEvent(timeline), timeline,
builder: ( ),
BuildContext context, builder: (
snapshot, BuildContext context,
) { snapshot,
final replyEvent = snapshot ) {
.hasData final replyEvent =
? snapshot.data! snapshot.hasData
: Event( ? snapshot.data!
eventId: event : Event(
.relationshipEventId!, eventId: event
content: { .relationshipEventId!,
'msgtype': content: {
'm.text', 'msgtype':
'body': '...', 'm.text',
}, 'body':
senderId: '...',
event.senderId, },
type: senderId: event
'm.room.message', .senderId,
room: event.room, type:
status: EventStatus 'm.room.message',
.sent, room: event
originServerTs: .room,
DateTime.now(), status:
); EventStatus
return Padding( .sent,
padding: originServerTs:
const EdgeInsets.only( DateTime
bottom: 4.0, .now(),
), );
child: InkWell( return Padding(
borderRadius: padding:
ReplyContent const EdgeInsets
.borderRadius, .only(
onTap: () => bottom: 4.0,
scrollToEventId(
replyEvent.eventId,
), ),
child: AbsorbPointer( child: InkWell(
child: ReplyContent( borderRadius:
replyEvent, ReplyContent
ownMessage: .borderRadius,
ownMessage, onTap: () =>
timeline: timeline, scrollToEventId(
replyEvent.eventId,
),
child: AbsorbPointer(
child: ReplyContent(
replyEvent,
ownMessage:
ownMessage,
timeline:
timeline,
),
), ),
), ),
), );
); },
},
),
MessageContent(
displayEvent,
textColor: textColor,
onInfoTab: onInfoTab,
borderRadius: borderRadius,
// #Pangea
selected: selected,
pangeaMessageEvent:
pangeaMessageEvent,
immersionMode: immersionMode,
toolbarController:
toolbarController,
// Pangea#
),
if (event.hasAggregatedEvents(
timeline,
RelationshipTypes.edit,
)
// #Pangea
||
(pangeaMessageEvent
?.showUseType ??
false)
// Pangea#
)
Padding(
padding:
const EdgeInsets.only(
top: 4.0,
), ),
child: Row( MessageContent(
mainAxisSize: displayEvent,
MainAxisSize.min, textColor: textColor,
children: [ onInfoTab: onInfoTab,
// #Pangea borderRadius: borderRadius,
if (pangeaMessageEvent // #Pangea
?.showUseType ?? selected: selected,
false) ...[ pangeaMessageEvent:
pangeaMessageEvent! pangeaMessageEvent,
.msgUseType immersionMode: immersionMode,
.iconView( isOverlay: isOverlay,
context, controller: controller,
textColor // Pangea#
.withAlpha(164), ),
), if (event.hasAggregatedEvents(
const SizedBox( timeline,
width: 4, RelationshipTypes
), .edit,
], )
if (event // #Pangea
.hasAggregatedEvents( ||
timeline, (pangeaMessageEvent
RelationshipTypes.edit, ?.showUseType ??
)) ...[ false)
// Pangea# // Pangea#
Icon( )
Icons.edit_outlined, Padding(
color: textColor padding:
.withAlpha(164), const EdgeInsets.only(
size: 14, top: 4.0,
), ),
Text( child: Row(
' - ${displayEvent.originServerTs.localizedTimeShort(context)}', mainAxisSize:
style: TextStyle( MainAxisSize.min,
children: [
// #Pangea
if (pangeaMessageEvent
?.showUseType ??
false) ...[
pangeaMessageEvent!
.msgUseType
.iconView(
context,
textColor
.withAlpha(164),
),
const SizedBox(
width: 4,
),
],
if (event
.hasAggregatedEvents(
timeline,
RelationshipTypes
.edit,
)) ...[
// Pangea#
Icon(
Icons.edit_outlined,
color: textColor color: textColor
.withAlpha(164), .withAlpha(164),
fontSize: 12, size: 14,
), ),
), Text(
' - ${displayEvent.originServerTs.localizedTimeShort(context)}',
style: TextStyle(
color: textColor
.withAlpha(
164,
),
fontSize: 12,
),
),
],
], ],
], ),
), ),
), ],
], ),
), ),
), ),
), ),
), ),
), ),
), ),
), ],
], ),
), ),
), ],
], ),
), ],
], ),
), );
); },
}, ),
); );
Widget container; Widget container;
final showReceiptsRow = final showReceiptsRow =
@ -544,7 +597,10 @@ class Message extends StatelessWidget {
crossAxisAlignment: crossAxisAlignment:
ownMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start, ownMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
if (displayTime || selected) // #Pangea
// if (displayTime || selected)
if ((displayTime || selected) && !isOverlay)
// Pangea#
Padding( Padding(
padding: displayTime padding: displayTime
? const EdgeInsets.symmetric(vertical: 8.0) ? const EdgeInsets.symmetric(vertical: 8.0)
@ -595,7 +651,8 @@ class Message extends StatelessWidget {
children: [ children: [
if (pangeaMessageEvent?.showMessageButtons ?? false) if (pangeaMessageEvent?.showMessageButtons ?? false)
MessageButtons( MessageButtons(
toolbarController: toolbarController, controller: controller,
pangeaMessageEvent: pangeaMessageEvent!,
), ),
MessageReactions(event, timeline), MessageReactions(event, timeline),
], ],

@ -1,16 +1,15 @@
import 'dart:math'; import 'dart:math';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/events/video_player.dart'; import 'package:fluffychat/pages/chat/events/video_player.dart';
import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/widgets/chat/message_context_menu.dart';
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
import 'package:fluffychat/pangea/widgets/igc/pangea_rich_text.dart'; import 'package:fluffychat/pangea/widgets/igc/pangea_rich_text.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/avatar.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
@ -37,8 +36,8 @@ class MessageContent extends StatelessWidget {
//here rather than passing the choreographer? pangea rich text, a widget //here rather than passing the choreographer? pangea rich text, a widget
//further down in the chain is also using pangeaController so its not constant //further down in the chain is also using pangeaController so its not constant
final bool immersionMode; final bool immersionMode;
final ToolbarDisplayController? toolbarController;
final bool isOverlay; final bool isOverlay;
final ChatController controller;
// Pangea# // Pangea#
const MessageContent( const MessageContent(
@ -50,8 +49,8 @@ class MessageContent extends StatelessWidget {
required this.selected, required this.selected,
this.pangeaMessageEvent, this.pangeaMessageEvent,
required this.immersionMode, required this.immersionMode,
required this.toolbarController,
this.isOverlay = false, this.isOverlay = false,
required this.controller,
// Pangea# // Pangea#
required this.borderRadius, required this.borderRadius,
}); });
@ -306,45 +305,34 @@ class MessageContent extends StatelessWidget {
style: messageTextStyle, style: messageTextStyle,
pangeaMessageEvent: pangeaMessageEvent!, pangeaMessageEvent: pangeaMessageEvent!,
immersionMode: immersionMode, immersionMode: immersionMode,
toolbarController: toolbarController, isOverlay: isOverlay,
controller: controller,
), ),
); );
} else if (pangeaMessageEvent != null) { }
toolbarController?.toolbar?.textSelection.setMessageText(
(event.getDisplayEvent(pangeaMessageEvent!.timeline).body), if (isOverlay) {
controller.textSelection.setMessageText(
event.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!),
hideReply: true,
),
); );
} }
return SelectableLinkify( return SelectableLinkify(
onSelectionChanged: (selection, cause) { onSelectionChanged: (selection, cause) {
if (cause == SelectionChangedCause.longPress && if (isOverlay) {
toolbarController != null && controller.textSelection.onTextSelection(selection);
pangeaMessageEvent != null &&
!(toolbarController!.highlighted) &&
!selected) {
return;
} }
toolbarController?.toolbar?.textSelection
.onTextSelection(selection);
}, },
onTap: () => toolbarController?.showToolbar(context), onTap: () {
contextMenuBuilder: (context, state) => if (pangeaMessageEvent != null && !isOverlay) {
(toolbarController?.highlighted ?? false) HapticFeedback.mediumImpact();
? const SizedBox.shrink() controller.showToolbar(pangeaMessageEvent!);
: MessageContextMenu.contextMenuOverride( }
context: context, },
textSelection: state, enableInteractiveSelection: isOverlay,
onDefine: () => toolbarController?.showToolbar(
context,
mode: MessageMode.definition,
),
onListen: () => toolbarController?.showToolbar(
context,
mode: MessageMode.textToSpeech,
),
),
enableInteractiveSelection:
toolbarController?.highlighted ?? false,
// Pangea# // Pangea#
text: event.calcLocalizedBodyFallback( text: event.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!), MatrixLocals(L10n.of(context)!),

@ -83,6 +83,9 @@ class PangeaAnyState {
// String chatViewTargetKey(String? roomId) => "chatViewKey$roomId"; // String chatViewTargetKey(String? roomId) => "chatViewKey$roomId";
// LayerLinkAndKey chatViewLinkAndKey(String? roomId) => // LayerLinkAndKey chatViewLinkAndKey(String? roomId) =>
// layerLinkAndKey(chatViewTargetKey(roomId)); // layerLinkAndKey(chatViewTargetKey(roomId));
RenderBox? getRenderBox(String key) =>
layerLinkAndKey(key).key.currentContext?.findRenderObject() as RenderBox?;
} }
class LayerLinkAndKey { class LayerLinkAndKey {

@ -1,27 +1,27 @@
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/enum/message_mode_enum.dart'; import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class MessageButtons extends StatelessWidget { class MessageButtons extends StatelessWidget {
final ToolbarDisplayController? toolbarController; final ChatController controller;
final PangeaMessageEvent pangeaMessageEvent;
const MessageButtons({ const MessageButtons({
super.key, super.key,
this.toolbarController, required this.controller,
required this.pangeaMessageEvent,
}); });
void showActivity(BuildContext context) { void showActivity(BuildContext context) {
toolbarController?.showToolbar( controller.showToolbar(
context, pangeaMessageEvent,
mode: MessageMode.practiceActivity, mode: MessageMode.practiceActivity,
); );
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (toolbarController == null) {
return const SizedBox.shrink();
}
return Padding( return Padding(
padding: const EdgeInsets.only(right: 8.0), padding: const EdgeInsets.only(right: 8.0),
child: Row( child: Row(

@ -1,195 +1,220 @@
import 'dart:async';
import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/events/message.dart';
import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/utils/any_state_holder.dart'; import 'package:fluffychat/pangea/widgets/chat/message_text_selection.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
import 'package:fluffychat/pangea/widgets/chat/overlay_footer.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_header.dart';
import 'package:fluffychat/pangea/widgets/chat/overlay_message.dart'; import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
class MessageSelectionOverlay extends StatelessWidget { class MessageSelectionOverlay extends StatefulWidget {
final ChatController controller; final ChatController controller;
final ToolbarDisplayController toolbarController; final Event event;
final Function closeToolbar;
final Widget toolbar;
final PangeaMessageEvent pangeaMessageEvent; final PangeaMessageEvent pangeaMessageEvent;
final bool ownMessage; final MessageMode? initialMode;
final bool immersionMode; final MessageTextSelection textSelection;
final String targetId;
const MessageSelectionOverlay({ const MessageSelectionOverlay({
required this.controller, required this.controller,
required this.closeToolbar, required this.event,
required this.toolbar,
required this.pangeaMessageEvent, required this.pangeaMessageEvent,
required this.immersionMode, required this.textSelection,
required this.ownMessage, this.initialMode,
required this.targetId,
required this.toolbarController,
super.key, super.key,
}); });
@override @override
Widget build(BuildContext context) { MessageSelectionOverlayState createState() => MessageSelectionOverlayState();
final LayerLinkAndKey layerLinkAndKey = }
MatrixState.pAnyState.layerLinkAndKey(targetId);
final targetRenderBox = class MessageSelectionOverlayState extends State<MessageSelectionOverlay> {
layerLinkAndKey.key.currentContext?.findRenderObject(); double overlayBottomOffset = -1;
double adjustedOverlayBottomOffset = -1;
double center = 290; Size? messageSize;
double? left; Offset? messageOffset;
double? right;
bool showDown = false; final StreamController _completeAnimationStream =
final double footerSize = PlatformInfos.isMobile StreamController.broadcast();
? PlatformInfos.isIOS
? 128 @override
: 108 void initState() {
: 143; super.initState();
final double headerSize = PlatformInfos.isMobile }
? PlatformInfos.isIOS
? 121 @override
: 84 void didChangeDependencies() {
: 77; super.didChangeDependencies();
final double stackSize =
MediaQuery.of(context).size.height - footerSize - headerSize; // position the overlay directly over the underlying message
setOverlayBottomOffset();
// wait for the toolbar to animate to full height
_completeAnimationStream.stream.first.then((_) {
if (toolbarHeight == null ||
messageSize == null ||
messageOffset == null) {
return;
}
// Once the toolbar has fully expanded, adjust
// the overlay's position if there's an overflow
final overlayTopOffset = messageOffset!.dy - toolbarHeight!;
final bool hasHeaderOverflow = overlayTopOffset < headerHeight;
final bool hasFooterOverflow = overlayBottomOffset < footerHeight;
if (hasHeaderOverflow) {
final overlayHeight = toolbarHeight! + messageSize!.height;
adjustedOverlayBottomOffset = screenHeight -
overlayHeight -
footerHeight -
MediaQuery.of(context).padding.bottom;
} else if (hasFooterOverflow) {
adjustedOverlayBottomOffset = footerHeight;
}
setState(() {});
});
}
@override
void dispose() {
_completeAnimationStream.close();
super.dispose();
}
void setOverlayBottomOffset() {
// Try to get the offset and size of the original message bubble.
// If it fails, return an empty SizedBox. For instance, this can fail if
// you change the screen size while the overlay is open.
try { try {
if (targetRenderBox != null) { final messageRenderBox = MatrixState.pAnyState.getRenderBox(
final Size transformTargetSize = (targetRenderBox as RenderBox).size; widget.event.eventId,
final Offset targetOffset = );
(targetRenderBox).localToGlobal(Offset.zero); if (messageRenderBox != null && messageRenderBox.hasSize) {
if (ownMessage) { messageSize = messageRenderBox.size;
right = MediaQuery.of(context).size.width - messageOffset = messageRenderBox.localToGlobal(Offset.zero);
targetOffset.dx - final messageTopOffset = messageOffset!.dy;
transformTargetSize.width; overlayBottomOffset =
} else { screenHeight - messageTopOffset - messageSize!.height;
left =
targetOffset.dx - (FluffyThemes.isColumnMode(context) ? 425 : 1);
}
showDown = targetOffset.dy + transformTargetSize.height / 2 <=
headerSize + stackSize / 2;
center = targetOffset.dy -
headerSize +
(showDown ? transformTargetSize.height + 3 : (-3));
// If top of selected message extends below header
if (targetOffset.dy <= headerSize) {
center = transformTargetSize.height + 3;
showDown = true;
}
// If bottom of selected message extends below footer
else if (targetOffset.dy + transformTargetSize.height >=
headerSize + stackSize) {
center = stackSize - transformTargetSize.height - 3;
}
final double midpoint = headerSize + stackSize / 2;
// If message is too long,
// use default location to make full use of screen
if (transformTargetSize.height >= stackSize / 2 - 30) {
center = stackSize / 2 + (showDown ? -30 : 30);
}
// If message is not too long, but too close
// to center of screen, scroll closer to edges
else if (targetOffset.dy + transformTargetSize.height > midpoint - 30 &&
targetOffset.dy < midpoint + 30) {
final double scrollUp = midpoint + 30 - targetOffset.dy;
final double scrollDown =
targetOffset.dy + transformTargetSize.height - (midpoint - 30);
final double minScroll =
controller.scrollController.position.minScrollExtent;
final double maxScroll =
controller.scrollController.position.maxScrollExtent;
final double currentOffset = controller.scrollController.offset;
// If can scroll up, scroll up
if (currentOffset + scrollUp < maxScroll) {
controller.scrollController.animateTo(
currentOffset + scrollUp,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
);
showDown = false;
center = stackSize / 2 + 27;
}
// Else if can scroll down, scroll down
else if (currentOffset - scrollDown > minScroll) {
controller.scrollController.animateTo(
currentOffset - scrollDown,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
);
showDown = true;
center = stackSize / 2 - 27;
}
// Neither scrolling works; leave message as-is,
// and use centered toolbar location
else {
center = stackSize / 2 + (showDown ? -30 : 30);
}
}
} }
} catch (err) { } catch (err) {
controller.showEmojiPicker = false; overlayBottomOffset = adjustedOverlayBottomOffset = -1;
controller.selectedEvents.clear(); } finally {
MatrixState.pAnyState.closeAllOverlays(); setState(() {});
ErrorHandler.logError(e: err, s: StackTrace.current); }
// throw L10n.of(context)!.toolbarError; }
return const SizedBox();
// height of the reply/forward bar + the reaction picker + contextual padding
double get footerHeight =>
48 + 56 + (FluffyThemes.isColumnMode(context) ? 16.0 : 8.0);
double get headerHeight =>
(Theme.of(context).appBarTheme.toolbarHeight ?? 56) +
MediaQuery.of(context).padding.top;
double get screenHeight => MediaQuery.of(context).size.height;
double? get toolbarHeight {
try {
final toolbarRenderBox = MatrixState.pAnyState.getRenderBox(
'${widget.pangeaMessageEvent.eventId}-toolbar',
);
return toolbarRenderBox?.size.height;
} catch (e) {
return null;
}
}
@override
Widget build(BuildContext context) {
if (overlayBottomOffset == -1) {
return const SizedBox.shrink();
} }
final Widget overlayMessage = OverlayMessage( final overlayMessage = ConstrainedBox(
pangeaMessageEvent.event, constraints: const BoxConstraints(
timeline: pangeaMessageEvent.timeline, maxWidth: FluffyThemes.columnWidth * 2.5,
immersionMode: immersionMode, ),
ownMessage: pangeaMessageEvent.ownMessage, child: Material(
toolbarController: toolbarController, type: MaterialType.transparency,
width: 290, child: Column(
showDown: showDown, mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: widget.pangeaMessageEvent.ownMessage
? MainAxisAlignment.end
: MainAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(
left: widget.pangeaMessageEvent.ownMessage
? 0
: Avatar.defaultSize + 16,
right: widget.pangeaMessageEvent.ownMessage ? 8 : 0,
),
child: MessageToolbar(
pangeaMessageEvent: widget.pangeaMessageEvent,
controller: widget.controller,
textSelection: widget.textSelection,
completeAnimationStream: _completeAnimationStream,
initialMode: widget.initialMode,
),
),
],
),
Message(
widget.event,
onSwipe: () => {},
onInfoTab: (_) => {},
onAvatarTab: (_) => {},
scrollToEventId: (_) => {},
immersionMode: widget.controller.choreographer.immersionMode,
controller: widget.controller,
timeline: widget.controller.timeline!,
isOverlay: true,
animateIn: false,
),
],
),
),
); );
return Expanded( return Expanded(
child: Column( child: Stack(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
crossAxisAlignment:
ownMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [ children: [
OverlayHeader( AnimatedPositioned(
controller: controller, duration: FluffyThemes.animationDuration,
closeToolbar: closeToolbar, left: 0,
), right: 0,
SizedBox( bottom: adjustedOverlayBottomOffset == -1
height: PlatformInfos.isAndroid ? 3 : 6, ? overlayBottomOffset
: adjustedOverlayBottomOffset,
child: Align(
alignment: Alignment.center,
child: overlayMessage,
),
), ),
Flexible( Align(
child: Stack( alignment: Alignment.bottomCenter,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Positioned( OverlayFooter(controller: widget.controller),
left: left,
right: right,
bottom: stackSize - center + 3,
child: showDown ? overlayMessage : toolbar,
),
Positioned(
left: left,
right: right,
top: center + 3,
child: showDown ? toolbar : overlayMessage,
),
], ],
), ),
), ),
SizedBox( Material(
height: PlatformInfos.isAndroid ? 3 : 6, child: OverlayHeader(controller: widget.controller),
), ),
OverlayFooter(controller: controller),
], ],
), ),
); );

@ -1,158 +1,35 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer';
import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/enum/message_mode_enum.dart'; import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/utils/overlay.dart';
import 'package:fluffychat/pangea/widgets/chat/message_audio_card.dart'; import 'package:fluffychat/pangea/widgets/chat/message_audio_card.dart';
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
import 'package:fluffychat/pangea/widgets/chat/message_speech_to_text_card.dart'; 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_text_selection.dart';
import 'package:fluffychat/pangea/widgets/chat/message_translation_card.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/message_unsubscribed_card.dart';
import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart'; import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart';
import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart';
import 'package:fluffychat/pangea/widgets/user_settings/p_language_dialog.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
class ToolbarDisplayController {
final PangeaMessageEvent pangeaMessageEvent;
final String targetId;
final bool immersionMode;
final ChatController controller;
final FocusNode focusNode = FocusNode();
Event? nextEvent;
Event? previousEvent;
MessageToolbar? toolbar;
String? overlayId;
double? messageWidth;
final toolbarModeStream = StreamController<MessageMode>.broadcast();
ToolbarDisplayController({
required this.pangeaMessageEvent,
required this.targetId,
required this.immersionMode,
required this.controller,
this.nextEvent,
this.previousEvent,
});
void closeToolbar() {
controller.clearSelectedEvents();
MatrixState.pAnyState.closeAllOverlays();
}
void setToolbar() {
toolbar ??= MessageToolbar(
textSelection: MessageTextSelection(),
room: pangeaMessageEvent.room,
toolbarModeStream: toolbarModeStream,
pangeaMessageEvent: pangeaMessageEvent,
immersionMode: immersionMode,
controller: controller,
);
}
void showToolbar(BuildContext context, {MessageMode? mode}) {
// Close keyboard, if open
if (controller.inputFocus.hasFocus) {
controller.inputFocus.unfocus();
return;
}
// Close emoji picker, if open
controller.showEmojiPicker = false;
if (highlighted) return;
if (!MatrixState.pangeaController.languageController.languagesSet) {
pLanguageDialog(context, () {});
return;
}
focusNode.requestFocus();
// I'm not sure why I put this here, but it causes the toolbar
// not to open immediately after clicking (user has to scroll or move their cursor)
// so I'm commenting it out for now
// WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
Widget? overlayEntry;
if (toolbar == null) return;
try {
overlayEntry = MessageSelectionOverlay(
controller: controller,
closeToolbar: closeToolbar,
toolbar: toolbar!,
pangeaMessageEvent: pangeaMessageEvent,
immersionMode: immersionMode,
ownMessage: pangeaMessageEvent.ownMessage,
targetId: targetId,
toolbarController: this,
);
} catch (err) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: err, s: StackTrace.current);
return;
}
OverlayUtil.showOverlay(
context: context,
child: overlayEntry,
transformTargetId: targetId,
targetAnchor: Alignment.center,
followerAnchor: Alignment.center,
backgroundColor: const Color.fromRGBO(0, 0, 0, 1).withAlpha(200),
closePrevOverlay:
MatrixState.pangeaController.subscriptionController.isSubscribed,
position: OverlayEnum.centered,
onDismiss: controller.clearSelectedEvents,
);
controller.onSelectMessage(pangeaMessageEvent.event);
if (MatrixState.pAnyState.entries.isNotEmpty) {
overlayId = MatrixState.pAnyState.entries.last.hashCode.toString();
}
if (mode != null) {
Future.delayed(
const Duration(milliseconds: 100),
() => toolbarModeStream.add(mode),
);
}
}
bool get highlighted {
if (overlayId == null) return false;
if (MatrixState.pAnyState.entries.isEmpty) {
overlayId = null;
return false;
}
return MatrixState.pAnyState.entries.last.hashCode.toString() == overlayId;
}
}
class MessageToolbar extends StatefulWidget { class MessageToolbar extends StatefulWidget {
final MessageTextSelection textSelection; final MessageTextSelection textSelection;
final Room room;
final PangeaMessageEvent pangeaMessageEvent; final PangeaMessageEvent pangeaMessageEvent;
final StreamController<MessageMode> toolbarModeStream;
final bool immersionMode;
final ChatController controller; final ChatController controller;
final MessageMode? initialMode;
final StreamController completeAnimationStream;
const MessageToolbar({ const MessageToolbar({
super.key, super.key,
required this.textSelection, required this.textSelection,
required this.room,
required this.pangeaMessageEvent, required this.pangeaMessageEvent,
required this.toolbarModeStream,
required this.immersionMode,
required this.controller, required this.controller,
required this.completeAnimationStream,
this.initialMode,
}); });
@override @override
@ -164,7 +41,6 @@ class MessageToolbarState extends State<MessageToolbar> {
MessageMode? currentMode; MessageMode? currentMode;
bool updatingMode = false; bool updatingMode = false;
late StreamSubscription<String?> selectionStream; late StreamSubscription<String?> selectionStream;
late StreamSubscription<MessageMode> toolbarModeStream;
void updateMode(MessageMode newMode) { void updateMode(MessageMode newMode) {
//Early exit from the function if the widget has been unmounted to prevent updates on an inactive widget. //Early exit from the function if the widget has been unmounted to prevent updates on an inactive widget.
@ -203,7 +79,7 @@ class MessageToolbarState extends State<MessageToolbar> {
toolbarContent = MessageUnsubscribedCard( toolbarContent = MessageUnsubscribedCard(
languageTool: newMode.title(context), languageTool: newMode.title(context),
mode: newMode, mode: newMode,
toolbarModeStream: widget.toolbarModeStream, controller: this,
); );
} else { } else {
switch (currentMode) { switch (currentMode) {
@ -242,7 +118,7 @@ class MessageToolbarState extends State<MessageToolbar> {
debugPrint("show translation"); debugPrint("show translation");
toolbarContent = MessageTranslationCard( toolbarContent = MessageTranslationCard(
messageEvent: widget.pangeaMessageEvent, messageEvent: widget.pangeaMessageEvent,
immersionMode: widget.immersionMode, immersionMode: widget.controller.choreographer.immersionMode,
selection: widget.textSelection, selection: widget.textSelection,
); );
} }
@ -275,7 +151,7 @@ class MessageToolbarState extends State<MessageToolbar> {
fullText: widget.textSelection.messageText, fullText: widget.textSelection.messageText,
fullTextLang: widget.pangeaMessageEvent.messageDisplayLangCode, fullTextLang: widget.pangeaMessageEvent.messageDisplayLangCode,
hasInfo: true, hasInfo: true,
room: widget.room, room: widget.controller.room,
); );
} }
@ -294,20 +170,20 @@ class MessageToolbarState extends State<MessageToolbar> {
super.initState(); super.initState();
widget.textSelection.selectedText = null; widget.textSelection.selectedText = null;
toolbarModeStream = widget.toolbarModeStream.stream.listen((mode) {
updateMode(mode);
});
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
if (widget.pangeaMessageEvent.isAudioMessage) { if (widget.pangeaMessageEvent.isAudioMessage) {
updateMode(MessageMode.speechToText); updateMode(MessageMode.speechToText);
return; return;
} }
MatrixState.pangeaController.userController.profile.userSettings if (widget.initialMode != null) {
.autoPlayMessages updateMode(widget.initialMode!);
? updateMode(MessageMode.textToSpeech) } else {
: updateMode(MessageMode.translation); MatrixState.pangeaController.userController.profile.userSettings
.autoPlayMessages
? updateMode(MessageMode.textToSpeech)
: updateMode(MessageMode.translation);
}
}); });
Timer? timer; Timer? timer;
@ -330,22 +206,37 @@ class MessageToolbarState extends State<MessageToolbar> {
@override @override
void dispose() { void dispose() {
selectionStream.cancel(); selectionStream.cancel();
toolbarModeStream.cancel();
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double maxHeight = (MediaQuery.of(context).size.height - final buttonRow = Row(
(PlatformInfos.isWeb mainAxisSize: MainAxisSize.min,
? 217 children: MessageMode.values
: PlatformInfos.isIOS .map(
? 262 (mode) => mode.isValidMode(widget.pangeaMessageEvent.event)
: 198)) / ? Tooltip(
2 + message: mode.tooltip(context),
30; child: IconButton(
icon: Icon(mode.icon),
color: mode.iconColor(
widget.pangeaMessageEvent,
currentMode,
context,
),
onPressed: () => updateMode(mode),
),
)
: const SizedBox.shrink(),
)
.toList(),
);
return Material( return Material(
key: MatrixState.pAnyState
.layerLinkAndKey('${widget.pangeaMessageEvent.eventId}-toolbar')
.key,
type: MaterialType.transparency, type: MaterialType.transparency,
child: Container( child: Container(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
@ -359,63 +250,26 @@ class MessageToolbarState extends State<MessageToolbar> {
Radius.circular(25), Radius.circular(25),
), ),
), ),
constraints: BoxConstraints(
maxWidth: 290,
minWidth: 290,
maxHeight: maxHeight,
),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Container( if (toolbarContent != null)
constraints: BoxConstraints( Container(
minWidth: 290, padding: const EdgeInsets.fromLTRB(8, 8, 8, 16),
maxHeight: maxHeight - 72, constraints: const BoxConstraints(
), maxWidth: 275,
child: SingleChildScrollView( minWidth: 275,
child: AnimatedSize( maxHeight: 250,
duration: FluffyThemes.animationDuration, ),
child: Column( child: SingleChildScrollView(
children: [ child: AnimatedSize(
Padding( duration: FluffyThemes.animationDuration,
padding: const EdgeInsets.all(8.0), child: toolbarContent,
child: toolbarContent ?? const SizedBox(), onEnd: () => widget.completeAnimationStream.add(null),
),
SizedBox(height: toolbarContent == null ? 0 : 20),
],
), ),
), ),
), ),
), buttonRow,
Row(
mainAxisSize: MainAxisSize.min,
children: MessageMode.values.map((mode) {
if ([
MessageMode.definition,
MessageMode.textToSpeech,
MessageMode.translation,
].contains(mode) &&
widget.pangeaMessageEvent.isAudioMessage) {
return const SizedBox.shrink();
}
if (mode == MessageMode.speechToText &&
!widget.pangeaMessageEvent.isAudioMessage) {
return const SizedBox.shrink();
}
return Tooltip(
message: mode.tooltip(context),
child: IconButton(
icon: Icon(mode.icon),
color: mode.iconColor(
widget.pangeaMessageEvent,
currentMode,
context,
),
onPressed: () => updateMode(mode),
),
);
}).toList(),
),
], ],
), ),
), ),

@ -1,7 +1,6 @@
import 'dart:async';
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/utils/bot_style.dart'; import 'package:fluffychat/pangea/utils/bot_style.dart';
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
@ -11,13 +10,13 @@ import '../../enum/message_mode_enum.dart';
class MessageUnsubscribedCard extends StatelessWidget { class MessageUnsubscribedCard extends StatelessWidget {
final String languageTool; final String languageTool;
final MessageMode mode; final MessageMode mode;
final StreamController<MessageMode> toolbarModeStream; final MessageToolbarState controller;
const MessageUnsubscribedCard({ const MessageUnsubscribedCard({
super.key, super.key,
required this.languageTool, required this.languageTool,
required this.mode, required this.mode,
required this.toolbarModeStream, required this.controller,
}); });
@override @override
@ -29,7 +28,7 @@ class MessageUnsubscribedCard extends StatelessWidget {
if (inTrialWindow) { if (inTrialWindow) {
MatrixState.pangeaController.subscriptionController MatrixState.pangeaController.subscriptionController
.activateNewUserTrial(); .activateNewUserTrial();
toolbarModeStream.add(mode); controller.updateMode(mode);
} else { } else {
MatrixState.pangeaController.subscriptionController MatrixState.pangeaController.subscriptionController
.showPaywall(context); .showPaywall(context);
@ -49,7 +48,7 @@ class MessageUnsubscribedCard extends StatelessWidget {
child: TextButton( child: TextButton(
onPressed: onButtonPress, onPressed: onButtonPress,
style: ButtonStyle( style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>( backgroundColor: WidgetStateProperty.all<Color>(
(AppConfig.primaryColor).withOpacity(0.1), (AppConfig.primaryColor).withOpacity(0.1),
), ),
), ),

@ -2,24 +2,23 @@ import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/chat_input_row.dart'; import 'package:fluffychat/pages/chat/chat_input_row.dart';
import 'package:fluffychat/pages/chat/reactions_picker.dart'; import 'package:fluffychat/pages/chat/reactions_picker.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class OverlayFooter extends StatelessWidget { class OverlayFooter extends StatelessWidget {
ChatController controller; final ChatController controller;
OverlayFooter({ const OverlayFooter({
required this.controller, required this.controller,
super.key, super.key,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 18.0 : 10.0; final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0;
return Container( return Container(
margin: EdgeInsets.only( margin: EdgeInsets.only(
bottom: PlatformInfos.isAndroid ? 0 : bottomSheetPadding, bottom: bottomSheetPadding,
left: bottomSheetPadding, left: bottomSheetPadding,
right: bottomSheetPadding, right: bottomSheetPadding,
), ),
@ -42,13 +41,6 @@ class OverlayFooter extends StatelessWidget {
], ],
), ),
), ),
SizedBox(
height: FluffyThemes.isColumnMode(context)
? 15.0
: PlatformInfos.isAndroid
? 0
: 8.0,
),
], ],
), ),
); );

@ -1,148 +1,92 @@
import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/chat_app_bar_title.dart'; import 'package:fluffychat/pages/chat/chat_app_bar_title.dart';
import 'package:fluffychat/pangea/utils/overlay.dart'; import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
class OverlayHeader extends StatelessWidget { class OverlayHeader extends StatelessWidget {
ChatController controller; final ChatController controller;
Function closeToolbar;
OverlayHeader({ const OverlayHeader({
required this.controller, required this.controller,
required this.closeToolbar,
super.key, super.key,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Event selectedEvent = controller.selectedEvents.single; return Column(
mainAxisSize: MainAxisSize.min,
return AppBar( children: [
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, AppBar(
actionsIconTheme: IconThemeData( actionsIconTheme: IconThemeData(
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
),
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () => closeToolbar(),
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) leading: IconButton(
IconButton( icon: const Icon(Icons.close),
icon: const Icon(Icons.delete_outlined), onPressed: () {
tooltip: L10n.of(context)!.redactMessage, controller.clearSelectedEvents();
onPressed: controller.redactEventsAction, MatrixState.pAnyState.closeAllOverlays();
},
tooltip: L10n.of(context)!.close,
color: Theme.of(context).colorScheme.primary,
), ),
IconButton( titleSpacing: 0,
padding: const EdgeInsets.only(bottom: 6), title: ChatAppBarTitle(controller),
icon: Icon( actions: [
Icons.more_horiz, if (controller.canEditSelectedEvents)
color: Theme.of(context).colorScheme.onSurface, IconButton(
), icon: const Icon(Icons.edit_outlined),
onPressed: () => showPopup(context), tooltip: L10n.of(context)!.edit,
), onPressed: controller.editSelectedEventAction,
], ),
); if (controller.selectedEvents.length == 1 &&
} controller.selectedEvents.single.messageType ==
MessageTypes.Text)
void showPopup(BuildContext context) { IconButton(
OverlayUtil.showOverlay( icon: const Icon(Icons.copy_outlined),
context: context, tooltip: L10n.of(context)!.copy,
child: SelectionPopup(controller: controller), onPressed: controller.copyEventsAction,
transformTargetId: "", ),
targetAnchor: Alignment.center, if (controller.canSaveSelectedEvent)
followerAnchor: Alignment.center, // Use builder context to correctly position the share dialog on iPad
closePrevOverlay: false, Builder(
position: OverlayEnum.topRight, builder: (context) => IconButton(
); icon: Icon(Icons.adaptive.share),
} tooltip: L10n.of(context)!.share,
} onPressed: () => controller.saveSelectedEvent(context),
),
class SelectionPopup extends StatelessWidget { ),
ChatController controller; if (controller.canPinSelectedEvents)
IconButton(
SelectionPopup({ icon: const Icon(Icons.push_pin_outlined),
required this.controller, onPressed: controller.pinEvent,
super.key, tooltip: L10n.of(context)!.pinMessage,
});
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
child: Container(
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextButton(
onPressed: controller.showEventInfo,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.info_outlined),
const SizedBox(width: 12),
Text(L10n.of(context)!.messageInfo),
],
), ),
), if (controller.canRedactSelectedEvents)
TextButton( IconButton(
onPressed: controller.reportEventAction, icon: const Icon(Icons.delete_outlined),
child: Row( tooltip: L10n.of(context)!.redactMessage,
mainAxisSize: MainAxisSize.min, onPressed: controller.redactEventsAction,
children: [ ),
const Icon( if (controller.selectedEvents.length == 1)
Icons.shield_outlined, IconButton(
color: Colors.red, icon: const Icon(Icons.info_outlined),
), tooltip: L10n.of(context)!.messageInfo,
const SizedBox(width: 12), onPressed: () {
Text(L10n.of(context)!.reportMessage), controller.showEventInfo();
], controller.clearSelectedEvents();
},
),
if (controller.selectedEvents.length == 1)
IconButton(
icon: const Icon(Icons.shield_outlined),
tooltip: L10n.of(context)!.reportMessage,
onPressed: controller.reportEventAction,
), ),
),
], ],
), ),
), ],
); );
} }
} }

@ -1,188 +0,0 @@
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/events/message_content.dart';
import 'package:fluffychat/pangea/enum/use_type.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import '../../../config/app_config.dart';
class OverlayMessage extends StatelessWidget {
final Event event;
final bool selected;
final Timeline timeline;
// final LanguageModel? selectedDisplayLang;
final bool immersionMode;
// final bool definitions;
final bool ownMessage;
final ToolbarDisplayController toolbarController;
final double? width;
final bool showDown;
const OverlayMessage(
this.event, {
this.selected = false,
required this.timeline,
required this.immersionMode,
required this.ownMessage,
required this.toolbarController,
required this.showDown,
this.width,
super.key,
});
@override
Widget build(BuildContext context) {
if (event.type != EventTypes.Message ||
event.messageType == EventTypes.KeyVerificationRequest) {
return const SizedBox.shrink();
}
var color = Theme.of(context).colorScheme.surfaceContainer;
final isLight = Theme.of(context).brightness == Brightness.light;
var lightness = isLight ? .05 : .2;
final textColor = ownMessage
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.onSurface;
const hardCorner = Radius.circular(4);
const roundedCorner = Radius.circular(AppConfig.borderRadius);
final borderRadius = BorderRadius.only(
topLeft: !showDown && !ownMessage ? hardCorner : roundedCorner,
topRight: !showDown && ownMessage ? hardCorner : roundedCorner,
bottomLeft: showDown && !ownMessage ? hardCorner : roundedCorner,
bottomRight: showDown && ownMessage ? hardCorner : roundedCorner,
);
final noBubble = {
MessageTypes.Video,
MessageTypes.Image,
MessageTypes.Sticker,
}.contains(event.messageType) &&
!event.redacted;
final noPadding = {
MessageTypes.File,
MessageTypes.Audio,
}.contains(event.messageType);
if (ownMessage) {
color = Theme.of(context).colorScheme.primary;
lightness = isLight ? .15 : .85;
}
// Make overlay a little darker/lighter than the message
color = Color.fromARGB(
color.alpha,
isLight || !ownMessage
? (color.red + lightness * (255 - color.red)).round()
: (color.red * lightness).round(),
isLight || !ownMessage
? (color.green + lightness * (255 - color.green)).round()
: (color.green * lightness).round(),
isLight || !ownMessage
? (color.blue + lightness * (255 - color.blue)).round()
: (color.blue * lightness).round(),
);
final double maxHeight = (MediaQuery.of(context).size.height -
(PlatformInfos.isWeb
? 228
: PlatformInfos.isIOS
? 258
: 198)) /
2 -
30;
final pangeaMessageEvent = PangeaMessageEvent(
event: event,
timeline: timeline,
ownMessage: ownMessage,
);
return Material(
color: noBubble ? Colors.transparent : color,
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: borderRadius,
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
),
padding: noBubble || noPadding
? EdgeInsets.zero
: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
constraints: BoxConstraints(
maxWidth: width ?? FluffyThemes.columnWidth * 1.25,
maxHeight: maxHeight,
),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: MessageContent(
event.getDisplayEvent(timeline),
textColor: textColor,
borderRadius: borderRadius,
selected: selected,
pangeaMessageEvent: pangeaMessageEvent,
immersionMode: immersionMode,
toolbarController: toolbarController,
isOverlay: true,
),
),
if (event.hasAggregatedEvents(
timeline,
RelationshipTypes.edit,
) ||
(pangeaMessageEvent.showUseType))
Padding(
padding: const EdgeInsets.only(
top: 4.0,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (pangeaMessageEvent.showUseType) ...[
pangeaMessageEvent.msgUseType.iconView(
context,
textColor.withAlpha(164),
),
const SizedBox(width: 4),
],
if (event.hasAggregatedEvents(
timeline,
RelationshipTypes.edit,
)) ...[
Icon(
Icons.edit_outlined,
color: textColor.withAlpha(164),
size: 14,
),
Text(
' - ${event.getDisplayEvent(timeline).originServerTs.localizedTimeShort(context)}',
style: TextStyle(
color: textColor.withAlpha(164),
fontSize: 12,
),
),
],
],
),
),
],
),
),
),
);
}
}

@ -7,13 +7,15 @@ class ToolbarContentLoadingIndicator extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return Center(
child: SizedBox(
height: 14, height: 14,
width: 14, width: 14,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2.0, strokeWidth: 2.0,
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
); ),
);
} }
} }

@ -2,31 +2,31 @@ import 'dart:developer';
import 'dart:ui'; import 'dart:ui';
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/enum/instructions_enum.dart'; import 'package:fluffychat/pangea/enum/instructions_enum.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/models/representation_content_model.dart'; import 'package:fluffychat/pangea/models/representation_content_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/widgets/chat/message_context_menu.dart';
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../enum/message_mode_enum.dart';
import '../../models/pangea_match_model.dart'; import '../../models/pangea_match_model.dart';
class PangeaRichText extends StatefulWidget { class PangeaRichText extends StatefulWidget {
final PangeaMessageEvent pangeaMessageEvent; final PangeaMessageEvent pangeaMessageEvent;
final bool immersionMode; final bool immersionMode;
final ToolbarDisplayController? toolbarController;
final TextStyle? style; final TextStyle? style;
final bool isOverlay;
final ChatController controller;
const PangeaRichText({ const PangeaRichText({
super.key, super.key,
required this.pangeaMessageEvent, required this.pangeaMessageEvent,
required this.immersionMode, required this.immersionMode,
required this.toolbarController, required this.isOverlay,
required this.controller,
this.style, this.style,
}); });
@ -59,12 +59,11 @@ class PangeaRichTextState extends State<PangeaRichText> {
void _setTextSpan(String newTextSpan) { void _setTextSpan(String newTextSpan) {
try { try {
if (!mounted) return; // Early exit if the widget is no longer in the tree if (!mounted) return; // Early exit if the widget is no longer in the tree
widget.toolbarController?.toolbar?.textSelection.setMessageText(
newTextSpan,
);
setState(() { setState(() {
textSpan = newTextSpan; textSpan = newTextSpan;
if (widget.isOverlay) {
widget.controller.textSelection.setMessageText(textSpan);
}
}); });
} catch (error, stackTrace) { } catch (error, stackTrace) {
ErrorHandler.logError( ErrorHandler.logError(
@ -137,35 +136,16 @@ class PangeaRichTextState extends State<PangeaRichText> {
//TODO - take out of build function of every message //TODO - take out of build function of every message
final Widget richText = SelectableText.rich( final Widget richText = SelectableText.rich(
onSelectionChanged: (selection, cause) { onSelectionChanged: (selection, cause) {
if (cause == SelectionChangedCause.longPress && if (widget.isOverlay) {
!(widget.toolbarController?.highlighted ?? false) && widget.controller.textSelection.onTextSelection(selection);
!(widget.toolbarController?.controller.selectedEvents.any(
(e) => e.eventId == widget.pangeaMessageEvent.eventId,
) ??
false)) {
return;
} }
widget.toolbarController?.toolbar?.textSelection
.onTextSelection(selection);
}, },
onTap: () => widget.toolbarController?.showToolbar(context), onTap: () {
enableInteractiveSelection: if (!widget.isOverlay) {
widget.toolbarController?.highlighted ?? false, widget.controller.showToolbar(widget.pangeaMessageEvent);
contextMenuBuilder: (context, state) => }
widget.toolbarController?.highlighted ?? true },
? const SizedBox.shrink() enableInteractiveSelection: widget.isOverlay,
: MessageContextMenu.contextMenuOverride(
context: context,
textSelection: state,
onDefine: () => widget.toolbarController?.showToolbar(
context,
mode: MessageMode.definition,
),
onListen: () => widget.toolbarController?.showToolbar(
context,
mode: MessageMode.textToSpeech,
),
),
TextSpan( TextSpan(
text: textSpan, text: textSpan,
style: widget.style, style: widget.style,

Loading…
Cancel
Save