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 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:collection/collection.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
import 'package:file_picker/file_picker.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/setting_keys.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/event_info_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/constants/pangea_event_types.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/matrix_event_wrappers/pangea_message_event.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/overlay.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/user_settings/p_language_dialog.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/filtered_timeline_extension.dart';
@ -928,20 +929,17 @@ class ChatController extends State<ChatPageWithRoom>
}
void copyEventsAction() {
// #Pangea
MatrixState.pAnyState.closeAllOverlays();
// Pangea#
Clipboard.setData(ClipboardData(text: _getSelectedEventString()));
setState(() {
showEmojiPicker = false;
selectedEvents.clear();
// #Pangea
// selectedEvents.clear();
clearSelectedEvents();
// Pangea#
});
}
void reportEventAction() async {
// #Pangea
MatrixState.pAnyState.closeAllOverlays();
// Pangea#
final event = selectedEvents.single;
// #Pangea
clearSelectedEvents();
@ -1035,9 +1033,6 @@ class ChatController extends State<ChatPageWithRoom>
}
void redactEventsAction() async {
// #Pangea
MatrixState.pAnyState.closeAllOverlays();
// Pangea#
final reasonInput = selectedEvents.any((event) => event.status.isSent)
? await showTextInputDialog(
context: context,
@ -1086,6 +1081,9 @@ class ChatController extends State<ChatPageWithRoom>
},
);
}
// #Pangea
clearSelectedEvents();
// Pangea#
setState(() {
showEmojiPicker = false;
selectedEvents.clear();
@ -1133,9 +1131,6 @@ class ChatController extends State<ChatPageWithRoom>
}
void forwardEventsAction() async {
// #Pangea
MatrixState.pAnyState.closeAllOverlays();
// Pangea#
if (selectedEvents.length == 1) {
Matrix.of(context).shareContent =
selectedEvents.first.getDisplayEvent(timeline!).content;
@ -1169,7 +1164,7 @@ class ChatController extends State<ChatPageWithRoom>
selectedEvents.clear();
});
// #Pangea
MatrixState.pAnyState.closeAllOverlays();
clearSelectedEvents();
// Pangea
inputFocus.requestFocus();
}
@ -1283,39 +1278,32 @@ class ChatController extends State<ChatPageWithRoom>
}
void pickEmojiReactionAction(Iterable<Event> allReactionEvents) async {
// #Pangea
MatrixState.pAnyState.closeAllOverlays();
// Pangea#
_allReactionEvents = allReactionEvents;
emojiPickerType = EmojiPickerType.reaction;
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 {
final events = List<Event>.from(selectedEvents);
setState(() => selectedEvents.clear());
// #Pangea
MatrixState.pAnyState.closeAllOverlays();
// Pangea#
for (final event in events) {
await room.sendReaction(
event.eventId,
emoji!,
);
}
// #Pangea
clearSelectedEvents();
// Pangea#
}
void clearSelectedEvents() => setState(() {
// #Pangea
MatrixState.pAnyState.closeAllOverlays();
// Pangea#
selectedEvents.clear();
showEmojiPicker = false;
});
@ -1552,12 +1540,7 @@ class ChatController extends State<ChatPageWithRoom>
bool get isArchived =>
{Membership.leave, Membership.ban}.contains(room.membership);
void showEventInfo([Event? event])
// #Pangea
// =>
{
MatrixState.pAnyState.closeAllOverlays();
// Pangea#
void showEventInfo([Event? event]) {
(event ?? selectedEvents.single).showInfoDialog(context);
// #Pangea
clearSelectedEvents();
@ -1618,80 +1601,51 @@ class ChatController extends State<ChatPageWithRoom>
editEvent = null;
});
// #Pangea
final Map<String, PangeaMessageEvent> _pangeaMessageEvents = {};
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,
);
}
// #Pangea
MessageTextSelection textSelection = MessageTextSelection();
void setToolbarDisplayController(
String eventId, {
Event? nextEvent,
Event? previousEvent,
void showToolbar(
PangeaMessageEvent pangeaMessageEvent, {
MessageMode? mode,
}) {
final Event? event = timeline!.events.firstWhereOrNull(
(e) => e.eventId == eventId,
);
if (event == null || timeline == null) return;
if (_pangeaMessageEvents[eventId] == null) {
setPangeaMessageEvent(eventId);
if (_pangeaMessageEvents[eventId] == null) return;
// Close keyboard, if open
if (inputFocus.hasFocus && PlatformInfos.isMobile) {
inputFocus.unfocus();
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 {
_toolbarDisplayControllers[eventId] = ToolbarDisplayController(
targetId: event.eventId,
pangeaMessageEvent: _pangeaMessageEvents[eventId]!,
immersionMode: choreographer.immersionMode,
overlayEntry = MessageSelectionOverlay(
controller: this,
nextEvent: nextEvent,
previousEvent: previousEvent,
);
_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(),
},
event: pangeaMessageEvent.event,
pangeaMessageEvent: pangeaMessageEvent,
textSelection: textSelection,
);
} 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(
String eventId, {
Event? nextEvent,
Event? previousEvent,
}) {
if (_toolbarDisplayControllers[eventId] == null) {
setToolbarDisplayController(
eventId,
nextEvent: nextEvent,
previousEvent: previousEvent,
);
}
return _toolbarDisplayControllers[eventId];
OverlayUtil.showOverlay(
context: context,
child: overlayEntry,
transformTargetId: "",
backgroundColor: const Color.fromRGBO(0, 0, 0, 1).withAlpha(200),
closePrevOverlay:
MatrixState.pangeaController.subscriptionController.isSubscribed,
position: OverlayEnum.centered,
onDismiss: clearSelectedEvents,
);
}
// Pangea#

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

@ -188,7 +188,6 @@ class ChatEventList extends StatelessWidget {
longPressSelect: controller.selectedEvents.isNotEmpty,
// #Pangea
immersionMode: controller.choreographer.immersionMode,
definitions: controller.choreographer.definitionsEnabled,
controller: controller,
// Pangea#
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_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';
@ -36,100 +35,98 @@ class ChatView extends StatelessWidget {
const ChatView(this.controller, {super.key});
List<Widget> _appBarActions(BuildContext context) {
if (controller.selectMode) {
return [
if (controller.canEditSelectedEvents)
IconButton(
icon: const Icon(Icons.edit_outlined),
tooltip: L10n.of(context)!.edit,
onPressed: controller.editSelectedEventAction,
),
// #Pangea
if (controller.selectedEvents.length == 1 &&
controller.selectedEvents.single.messageType == MessageTypes.Text)
// Pangea#
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,
),
if (controller.selectedEvents.length == 1)
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 (controller.selectedEvents.single.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),
],
),
),
],
),
];
// #Pangea
} else {
return [
RoundTimer(controller: controller),
const SizedBox(
width: 10,
),
ChatSettingsPopupMenu(
controller.room,
(!controller.room.isDirectChat && !controller.room.isArchived),
),
];
}
// #Pangea
// if (controller.selectMode) {
// return [
// if (controller.canEditSelectedEvents)
// IconButton(
// icon: const Icon(Icons.edit_outlined),
// tooltip: L10n.of(context)!.edit,
// onPressed: controller.editSelectedEventAction,
// ),
// // #Pangea
// if (controller.selectedEvents.length == 1 &&
// controller.selectedEvents.single.messageType == MessageTypes.Text)
// // Pangea#
// 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,
// ),
// if (controller.selectedEvents.length == 1)
// 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 (controller.selectedEvents.single.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),
// ],
// ),
// ),
// ],
// ),
// ];
return [
RoundTimer(controller: controller),
const SizedBox(
width: 10,
),
ChatSettingsPopupMenu(
controller.room,
(!controller.room.isDirectChat && !controller.room.isArchived),
),
];
// else if (!controller.room.isArchived) {
// return [
// if (Matrix.of(context).voipPlugin != null &&
@ -196,28 +193,34 @@ class ChatView extends StatelessWidget {
}
return Scaffold(
appBar: AppBar(
actionsIconTheme: IconThemeData(
color: controller.selectedEvents.isEmpty
? null
: Theme.of(context).colorScheme.primary,
actionsIconTheme: const IconThemeData(
// #Pangea
// color: controller.selectedEvents.isEmpty
// ? 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,
title: ChatAppBarTitle(controller),
actions: _appBarActions(context),
@ -501,7 +504,6 @@ class ChatView extends StatelessWidget {
ITBar(
choreographer: controller.choreographer,
),
ReactionsPicker(controller),
ReplyDisplay(controller),
ChatInputRow(controller),
ChatEmojiPicker(controller),

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

@ -1,16 +1,15 @@
import 'dart:math';
import 'package:fluffychat/pages/chat/chat.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/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/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_linkify/flutter_linkify.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
//further down in the chain is also using pangeaController so its not constant
final bool immersionMode;
final ToolbarDisplayController? toolbarController;
final bool isOverlay;
final ChatController controller;
// Pangea#
const MessageContent(
@ -50,8 +49,8 @@ class MessageContent extends StatelessWidget {
required this.selected,
this.pangeaMessageEvent,
required this.immersionMode,
required this.toolbarController,
this.isOverlay = false,
required this.controller,
// Pangea#
required this.borderRadius,
});
@ -306,45 +305,34 @@ class MessageContent extends StatelessWidget {
style: messageTextStyle,
pangeaMessageEvent: pangeaMessageEvent!,
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(
onSelectionChanged: (selection, cause) {
if (cause == SelectionChangedCause.longPress &&
toolbarController != null &&
pangeaMessageEvent != null &&
!(toolbarController!.highlighted) &&
!selected) {
return;
if (isOverlay) {
controller.textSelection.onTextSelection(selection);
}
toolbarController?.toolbar?.textSelection
.onTextSelection(selection);
},
onTap: () => toolbarController?.showToolbar(context),
contextMenuBuilder: (context, state) =>
(toolbarController?.highlighted ?? false)
? const SizedBox.shrink()
: MessageContextMenu.contextMenuOverride(
context: context,
textSelection: state,
onDefine: () => toolbarController?.showToolbar(
context,
mode: MessageMode.definition,
),
onListen: () => toolbarController?.showToolbar(
context,
mode: MessageMode.textToSpeech,
),
),
enableInteractiveSelection:
toolbarController?.highlighted ?? false,
onTap: () {
if (pangeaMessageEvent != null && !isOverlay) {
HapticFeedback.mediumImpact();
controller.showToolbar(pangeaMessageEvent!);
}
},
enableInteractiveSelection: isOverlay,
// Pangea#
text: event.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!),

@ -83,6 +83,9 @@ class PangeaAnyState {
// String chatViewTargetKey(String? roomId) => "chatViewKey$roomId";
// LayerLinkAndKey chatViewLinkAndKey(String? roomId) =>
// layerLinkAndKey(chatViewTargetKey(roomId));
RenderBox? getRenderBox(String key) =>
layerLinkAndKey(key).key.currentContext?.findRenderObject() as RenderBox?;
}
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/widgets/chat/message_toolbar.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:flutter/material.dart';
class MessageButtons extends StatelessWidget {
final ToolbarDisplayController? toolbarController;
final ChatController controller;
final PangeaMessageEvent pangeaMessageEvent;
const MessageButtons({
super.key,
this.toolbarController,
required this.controller,
required this.pangeaMessageEvent,
});
void showActivity(BuildContext context) {
toolbarController?.showToolbar(
context,
controller.showToolbar(
pangeaMessageEvent,
mode: MessageMode.practiceActivity,
);
}
@override
Widget build(BuildContext context) {
if (toolbarController == null) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Row(

@ -1,195 +1,220 @@
import 'dart:async';
import 'package:fluffychat/config/themes.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/utils/any_state_holder.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/widgets/chat/message_text_selection.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_header.dart';
import 'package:fluffychat/pangea/widgets/chat/overlay_message.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
class MessageSelectionOverlay extends StatelessWidget {
class MessageSelectionOverlay extends StatefulWidget {
final ChatController controller;
final ToolbarDisplayController toolbarController;
final Function closeToolbar;
final Widget toolbar;
final Event event;
final PangeaMessageEvent pangeaMessageEvent;
final bool ownMessage;
final bool immersionMode;
final String targetId;
final MessageMode? initialMode;
final MessageTextSelection textSelection;
const MessageSelectionOverlay({
required this.controller,
required this.closeToolbar,
required this.toolbar,
required this.event,
required this.pangeaMessageEvent,
required this.immersionMode,
required this.ownMessage,
required this.targetId,
required this.toolbarController,
required this.textSelection,
this.initialMode,
super.key,
});
@override
Widget build(BuildContext context) {
final LayerLinkAndKey layerLinkAndKey =
MatrixState.pAnyState.layerLinkAndKey(targetId);
final targetRenderBox =
layerLinkAndKey.key.currentContext?.findRenderObject();
double center = 290;
double? left;
double? right;
bool showDown = false;
final double footerSize = PlatformInfos.isMobile
? PlatformInfos.isIOS
? 128
: 108
: 143;
final double headerSize = PlatformInfos.isMobile
? PlatformInfos.isIOS
? 121
: 84
: 77;
final double stackSize =
MediaQuery.of(context).size.height - footerSize - headerSize;
MessageSelectionOverlayState createState() => MessageSelectionOverlayState();
}
class MessageSelectionOverlayState extends State<MessageSelectionOverlay> {
double overlayBottomOffset = -1;
double adjustedOverlayBottomOffset = -1;
Size? messageSize;
Offset? messageOffset;
final StreamController _completeAnimationStream =
StreamController.broadcast();
@override
void initState() {
super.initState();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// 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 {
if (targetRenderBox != null) {
final Size transformTargetSize = (targetRenderBox as RenderBox).size;
final Offset targetOffset =
(targetRenderBox).localToGlobal(Offset.zero);
if (ownMessage) {
right = MediaQuery.of(context).size.width -
targetOffset.dx -
transformTargetSize.width;
} else {
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);
}
}
final messageRenderBox = MatrixState.pAnyState.getRenderBox(
widget.event.eventId,
);
if (messageRenderBox != null && messageRenderBox.hasSize) {
messageSize = messageRenderBox.size;
messageOffset = messageRenderBox.localToGlobal(Offset.zero);
final messageTopOffset = messageOffset!.dy;
overlayBottomOffset =
screenHeight - messageTopOffset - messageSize!.height;
}
} catch (err) {
controller.showEmojiPicker = false;
controller.selectedEvents.clear();
MatrixState.pAnyState.closeAllOverlays();
ErrorHandler.logError(e: err, s: StackTrace.current);
// throw L10n.of(context)!.toolbarError;
return const SizedBox();
overlayBottomOffset = adjustedOverlayBottomOffset = -1;
} finally {
setState(() {});
}
}
// 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(
pangeaMessageEvent.event,
timeline: pangeaMessageEvent.timeline,
immersionMode: immersionMode,
ownMessage: pangeaMessageEvent.ownMessage,
toolbarController: toolbarController,
width: 290,
showDown: showDown,
final overlayMessage = ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 2.5,
),
child: Material(
type: MaterialType.transparency,
child: Column(
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(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
crossAxisAlignment:
ownMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
child: Stack(
children: [
OverlayHeader(
controller: controller,
closeToolbar: closeToolbar,
),
SizedBox(
height: PlatformInfos.isAndroid ? 3 : 6,
AnimatedPositioned(
duration: FluffyThemes.animationDuration,
left: 0,
right: 0,
bottom: adjustedOverlayBottomOffset == -1
? overlayBottomOffset
: adjustedOverlayBottomOffset,
child: Align(
alignment: Alignment.center,
child: overlayMessage,
),
),
Flexible(
child: Stack(
Align(
alignment: Alignment.bottomCenter,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Positioned(
left: left,
right: right,
bottom: stackSize - center + 3,
child: showDown ? overlayMessage : toolbar,
),
Positioned(
left: left,
right: right,
top: center + 3,
child: showDown ? toolbar : overlayMessage,
),
OverlayFooter(controller: widget.controller),
],
),
),
SizedBox(
height: PlatformInfos.isAndroid ? 3 : 6,
Material(
child: OverlayHeader(controller: widget.controller),
),
OverlayFooter(controller: controller),
],
),
);

@ -1,158 +1,35 @@
import 'dart:async';
import 'dart:developer';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/chat.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/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_selection_overlay.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_translation_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/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:flutter/foundation.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 {
final MessageTextSelection textSelection;
final Room room;
final PangeaMessageEvent pangeaMessageEvent;
final StreamController<MessageMode> toolbarModeStream;
final bool immersionMode;
final ChatController controller;
final MessageMode? initialMode;
final StreamController completeAnimationStream;
const MessageToolbar({
super.key,
required this.textSelection,
required this.room,
required this.pangeaMessageEvent,
required this.toolbarModeStream,
required this.immersionMode,
required this.controller,
required this.completeAnimationStream,
this.initialMode,
});
@override
@ -164,7 +41,6 @@ class MessageToolbarState extends State<MessageToolbar> {
MessageMode? currentMode;
bool updatingMode = false;
late StreamSubscription<String?> selectionStream;
late StreamSubscription<MessageMode> toolbarModeStream;
void updateMode(MessageMode newMode) {
//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(
languageTool: newMode.title(context),
mode: newMode,
toolbarModeStream: widget.toolbarModeStream,
controller: this,
);
} else {
switch (currentMode) {
@ -242,7 +118,7 @@ class MessageToolbarState extends State<MessageToolbar> {
debugPrint("show translation");
toolbarContent = MessageTranslationCard(
messageEvent: widget.pangeaMessageEvent,
immersionMode: widget.immersionMode,
immersionMode: widget.controller.choreographer.immersionMode,
selection: widget.textSelection,
);
}
@ -275,7 +151,7 @@ class MessageToolbarState extends State<MessageToolbar> {
fullText: widget.textSelection.messageText,
fullTextLang: widget.pangeaMessageEvent.messageDisplayLangCode,
hasInfo: true,
room: widget.room,
room: widget.controller.room,
);
}
@ -294,20 +170,20 @@ class MessageToolbarState extends State<MessageToolbar> {
super.initState();
widget.textSelection.selectedText = null;
toolbarModeStream = widget.toolbarModeStream.stream.listen((mode) {
updateMode(mode);
});
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
if (widget.pangeaMessageEvent.isAudioMessage) {
updateMode(MessageMode.speechToText);
return;
}
MatrixState.pangeaController.userController.profile.userSettings
.autoPlayMessages
? updateMode(MessageMode.textToSpeech)
: updateMode(MessageMode.translation);
if (widget.initialMode != null) {
updateMode(widget.initialMode!);
} else {
MatrixState.pangeaController.userController.profile.userSettings
.autoPlayMessages
? updateMode(MessageMode.textToSpeech)
: updateMode(MessageMode.translation);
}
});
Timer? timer;
@ -330,22 +206,37 @@ class MessageToolbarState extends State<MessageToolbar> {
@override
void dispose() {
selectionStream.cancel();
toolbarModeStream.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final double maxHeight = (MediaQuery.of(context).size.height -
(PlatformInfos.isWeb
? 217
: PlatformInfos.isIOS
? 262
: 198)) /
2 +
30;
final buttonRow = Row(
mainAxisSize: MainAxisSize.min,
children: MessageMode.values
.map(
(mode) => mode.isValidMode(widget.pangeaMessageEvent.event)
? Tooltip(
message: mode.tooltip(context),
child: IconButton(
icon: Icon(mode.icon),
color: mode.iconColor(
widget.pangeaMessageEvent,
currentMode,
context,
),
onPressed: () => updateMode(mode),
),
)
: const SizedBox.shrink(),
)
.toList(),
);
return Material(
key: MatrixState.pAnyState
.layerLinkAndKey('${widget.pangeaMessageEvent.eventId}-toolbar')
.key,
type: MaterialType.transparency,
child: Container(
padding: const EdgeInsets.all(10),
@ -359,63 +250,26 @@ class MessageToolbarState extends State<MessageToolbar> {
Radius.circular(25),
),
),
constraints: BoxConstraints(
maxWidth: 290,
minWidth: 290,
maxHeight: maxHeight,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
constraints: BoxConstraints(
minWidth: 290,
maxHeight: maxHeight - 72,
),
child: SingleChildScrollView(
child: AnimatedSize(
duration: FluffyThemes.animationDuration,
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: toolbarContent ?? const SizedBox(),
),
SizedBox(height: toolbarContent == null ? 0 : 20),
],
if (toolbarContent != null)
Container(
padding: const EdgeInsets.fromLTRB(8, 8, 8, 16),
constraints: const BoxConstraints(
maxWidth: 275,
minWidth: 275,
maxHeight: 250,
),
child: SingleChildScrollView(
child: AnimatedSize(
duration: FluffyThemes.animationDuration,
child: toolbarContent,
onEnd: () => widget.completeAnimationStream.add(null),
),
),
),
),
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(),
),
buttonRow,
],
),
),

@ -1,7 +1,6 @@
import 'dart:async';
import 'package:fluffychat/config/app_config.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:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
@ -11,13 +10,13 @@ import '../../enum/message_mode_enum.dart';
class MessageUnsubscribedCard extends StatelessWidget {
final String languageTool;
final MessageMode mode;
final StreamController<MessageMode> toolbarModeStream;
final MessageToolbarState controller;
const MessageUnsubscribedCard({
super.key,
required this.languageTool,
required this.mode,
required this.toolbarModeStream,
required this.controller,
});
@override
@ -29,7 +28,7 @@ class MessageUnsubscribedCard extends StatelessWidget {
if (inTrialWindow) {
MatrixState.pangeaController.subscriptionController
.activateNewUserTrial();
toolbarModeStream.add(mode);
controller.updateMode(mode);
} else {
MatrixState.pangeaController.subscriptionController
.showPaywall(context);
@ -49,7 +48,7 @@ class MessageUnsubscribedCard extends StatelessWidget {
child: TextButton(
onPressed: onButtonPress,
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(
backgroundColor: WidgetStateProperty.all<Color>(
(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_input_row.dart';
import 'package:fluffychat/pages/chat/reactions_picker.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:flutter/material.dart';
class OverlayFooter extends StatelessWidget {
ChatController controller;
final ChatController controller;
OverlayFooter({
const OverlayFooter({
required this.controller,
super.key,
});
@override
Widget build(BuildContext context) {
final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 18.0 : 10.0;
final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0;
return Container(
margin: EdgeInsets.only(
bottom: PlatformInfos.isAndroid ? 0 : bottomSheetPadding,
bottom: bottomSheetPadding,
left: 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_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_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
class OverlayHeader extends StatelessWidget {
ChatController controller;
Function closeToolbar;
final ChatController controller;
OverlayHeader({
const OverlayHeader({
required this.controller,
required this.closeToolbar,
super.key,
});
@override
Widget build(BuildContext context) {
final Event selectedEvent = controller.selectedEvents.single;
return AppBar(
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
actionsIconTheme: IconThemeData(
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,
return Column(
mainAxisSize: MainAxisSize.min,
children: [
AppBar(
actionsIconTheme: IconThemeData(
color: Theme.of(context).colorScheme.primary,
),
if (controller.canRedactSelectedEvents)
IconButton(
icon: const Icon(Icons.delete_outlined),
tooltip: L10n.of(context)!.redactMessage,
onPressed: controller.redactEventsAction,
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
controller.clearSelectedEvents();
MatrixState.pAnyState.closeAllOverlays();
},
tooltip: L10n.of(context)!.close,
color: Theme.of(context).colorScheme.primary,
),
IconButton(
padding: const EdgeInsets.only(bottom: 6),
icon: Icon(
Icons.more_horiz,
color: Theme.of(context).colorScheme.onSurface,
),
onPressed: () => showPopup(context),
),
],
);
}
void showPopup(BuildContext context) {
OverlayUtil.showOverlay(
context: context,
child: SelectionPopup(controller: controller),
transformTargetId: "",
targetAnchor: Alignment.center,
followerAnchor: Alignment.center,
closePrevOverlay: false,
position: OverlayEnum.topRight,
);
}
}
class SelectionPopup extends StatelessWidget {
ChatController controller;
SelectionPopup({
required this.controller,
super.key,
});
@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),
],
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 (controller.selectedEvents.length == 1 &&
controller.selectedEvents.single.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,
),
),
TextButton(
onPressed: controller.reportEventAction,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.shield_outlined,
color: Colors.red,
),
const SizedBox(width: 12),
Text(L10n.of(context)!.reportMessage),
],
if (controller.canRedactSelectedEvents)
IconButton(
icon: const Icon(Icons.delete_outlined),
tooltip: L10n.of(context)!.redactMessage,
onPressed: controller.redactEventsAction,
),
if (controller.selectedEvents.length == 1)
IconButton(
icon: const Icon(Icons.info_outlined),
tooltip: L10n.of(context)!.messageInfo,
onPressed: () {
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
Widget build(BuildContext context) {
return SizedBox(
return Center(
child: SizedBox(
height: 14,
width: 14,
child: CircularProgressIndicator(
strokeWidth: 2.0,
color: Theme.of(context).colorScheme.primary,
),
);
),
);
}
}

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

Loading…
Cancel
Save