Merge pull request #548 from pangeachat/toolbar-selection

Combines toolbar with selection view
This commit is contained in:
ggurdin 2024-08-30 14:40:05 -04:00 committed by GitHub
commit 8ae98a84b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 785 additions and 810 deletions

View file

@ -3,7 +3,6 @@ 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';
@ -16,6 +15,7 @@ import 'package:fluffychat/pages/chat/recording_dialog.dart';
import 'package:fluffychat/pages/chat_details/chat_details.dart';
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.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';
@ -23,9 +23,12 @@ import 'package:fluffychat/pangea/models/representation_content_model.dart';
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
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';
@ -838,10 +841,23 @@ class ChatController extends State<ChatPageWithRoom>
});
}
void hideEmojiPicker() {
// #Pangea
// void hideEmojiPicker() {
void hideEmojiPicker({bool closeOverlay = false}) {
if (closeOverlay) {
MatrixState.pAnyState.closeOverlay();
}
// Pangea#
setState(() => showEmojiPicker = false);
}
// #Pangea
void hideOverlayEmojiPicker() {
MatrixState.pAnyState.closeOverlay();
setState(() => showEmojiPicker = false);
}
// Pangea
void emojiPickerAction() {
if (showEmojiPicker) {
inputFocus.requestFocus();
@ -887,12 +903,18 @@ class ChatController extends State<ChatPageWithRoom>
Clipboard.setData(ClipboardData(text: _getSelectedEventString()));
setState(() {
showEmojiPicker = false;
selectedEvents.clear();
// #Pangea
// selectedEvents.clear();
clearSelectedEvents();
// Pangea#
});
}
void reportEventAction() async {
final event = selectedEvents.single;
// #Pangea
clearSelectedEvents();
// Pangea#
final score = await showConfirmationDialog<int>(
context: context,
title: L10n.of(context)!.reportMessage,
@ -997,7 +1019,12 @@ class ChatController extends State<ChatPageWithRoom>
cancelLabel: L10n.of(context)!.cancel,
)
: <String>[];
if (reasonInput == null) return;
if (reasonInput == null) {
// #Pangea
clearSelectedEvents();
// Pangea#
return;
}
final reason = reasonInput.single.isEmpty ? null : reasonInput.single;
for (final event in selectedEvents) {
await showFutureLoadingDialog(
@ -1025,6 +1052,9 @@ class ChatController extends State<ChatPageWithRoom>
},
);
}
// #Pangea
clearSelectedEvents();
// Pangea#
setState(() {
showEmojiPicker = false;
selectedEvents.clear();
@ -1104,6 +1134,9 @@ class ChatController extends State<ChatPageWithRoom>
replyEvent = replyTo ?? selectedEvents.first;
selectedEvents.clear();
});
// #Pangea
clearSelectedEvents();
// Pangea
inputFocus.requestFocus();
}
@ -1216,6 +1249,9 @@ class ChatController extends State<ChatPageWithRoom>
}
void pickEmojiReactionAction(Iterable<Event> allReactionEvents) async {
// #Pangea
MatrixState.pAnyState.closeAllOverlays();
// Pangea#
_allReactionEvents = allReactionEvents;
emojiPickerType = EmojiPickerType.reaction;
setState(() => showEmojiPicker = true);
@ -1230,9 +1266,15 @@ class ChatController extends State<ChatPageWithRoom>
emoji!,
);
}
// #Pangea
clearSelectedEvents();
// Pangea#
}
void clearSelectedEvents() => setState(() {
// #Pangea
MatrixState.pAnyState.closeAllOverlays();
// Pangea#
selectedEvents.clear();
showEmojiPicker = false;
});
@ -1469,8 +1511,12 @@ class ChatController extends State<ChatPageWithRoom>
bool get isArchived =>
{Membership.leave, Membership.ban}.contains(room.membership);
void showEventInfo([Event? event]) =>
(event ?? selectedEvents.single).showInfoDialog(context);
void showEventInfo([Event? event]) {
(event ?? selectedEvents.single).showInfoDialog(context);
// #Pangea
clearSelectedEvents();
// Pangea#
}
void onPhoneButtonTap() async {
// VoIP required Android SDK 21
@ -1526,80 +1572,55 @@ class ChatController extends State<ChatPageWithRoom>
editEvent = null;
});
// #Pangea
final Map<String, PangeaMessageEvent> _pangeaMessageEvents = {};
final Map<String, ToolbarDisplayController> _toolbarDisplayControllers = {};
// #Pangea
MessageTextSelection textSelection = MessageTextSelection();
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(
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;
// select the message
onSelectMessage(pangeaMessageEvent.event);
HapticFeedback.mediumImpact();
// 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: OverlayPositionEnum.centered,
onDismiss: clearSelectedEvents,
);
}
// Pangea#

View file

@ -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,
),
),
),
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
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),
],
),
),
// #Pangea
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: FloatingActionButton(
onPressed: controller.hideEmojiPicker,
shape: const CircleBorder(),
mini: true,
child: const Icon(Icons.close),
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();
},
),
],
),
),
),
// Pangea#
],
),
)
: null,
// #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,
),
);
}
}

View file

@ -165,13 +165,15 @@ class ChatEventList extends StatelessWidget {
),
highlightMarker:
controller.scrollToEventIdMarker == event.eventId,
onSelect: controller.onSelectMessage,
// #Pangea
// onSelect: controller.onSelectMessage,
onSelect: (_) {},
// Pangea#
scrollToEventId: (String eventId) =>
controller.scrollToEventId(eventId),
longPressSelect: controller.selectedEvents.isNotEmpty,
// #Pangea
immersionMode: controller.choreographer.immersionMode,
definitions: controller.choreographer.definitionsEnabled,
controller: controller,
// Pangea#
selected: controller.selectedEvents

View file

@ -6,7 +6,6 @@ import 'package:fluffychat/pages/chat/chat_app_bar_title.dart';
import 'package:fluffychat/pages/chat/chat_emoji_picker.dart';
import 'package:fluffychat/pages/chat/chat_event_list.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';
@ -496,7 +495,6 @@ class ChatView extends StatelessWidget {
ITBar(
choreographer: controller.choreographer,
),
ReactionsPicker(controller),
ReplyDisplay(controller),
ChatInputRowWrapper(
controller: controller,

View file

@ -2,14 +2,13 @@ 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';
@ -39,8 +38,8 @@ class Message extends StatelessWidget {
final void Function()? resetAnimateIn;
// #Pangea
final bool immersionMode;
final bool definitions;
final ChatController controller;
final bool isOverlay;
// Pangea#
final Color? avatarPresenceBackgroundColor;
@ -63,21 +62,32 @@ 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) {
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();
@ -162,21 +172,6 @@ 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;
@ -203,8 +198,11 @@ class Message extends StatelessWidget {
left: 0,
right: 0,
child: InkWell(
onTap: () => onSelect(event),
onLongPress: () => onSelect(event),
// #Pangea
onTap: controller.clearSelectedEvents,
// onTap: () => onSelect(event),
// onLongPress: () => onSelect(event),
// Pangea#
borderRadius:
BorderRadius.circular(AppConfig.borderRadius / 2),
child: Material(
@ -228,17 +226,20 @@ class Message extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: rowMainAxisAlignment,
children: [
if (longPressSelect)
SizedBox(
height: 32,
width: Avatar.defaultSize,
child: Checkbox.adaptive(
value: selected,
shape: const CircleBorder(),
onChanged: (_) => onSelect(event),
),
)
else if (nextEventSameSender || ownMessage)
// #Pangea
// if (longPressSelect)
// SizedBox(
// height: 32,
// width: Avatar.defaultSize,
// child: Checkbox.adaptive(
// value: selected,
// shape: const CircleBorder(),
// onChanged: (_) => onSelect(event),
// ),
// )
// else if (nextEventSameSender || ownMessage)
if (nextEventSameSender || ownMessage || isOverlay)
// Pangea#
SizedBox(
width: Avatar.defaultSize,
child: Center(
@ -277,7 +278,10 @@ class Message extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (!nextEventSameSender)
// #Pangea
// if (!nextEventSameSender)
if (!nextEventSameSender && !isOverlay)
// Pangea#
Padding(
padding: const EdgeInsets.only(
left: 8.0,
@ -314,18 +318,18 @@ class Message extends StatelessWidget {
padding: const EdgeInsets.only(left: 8),
child: GestureDetector(
// #Pangea
onTap: () => toolbarController?.showToolbar(
context,
),
onTap: () => showToolbar(pangeaMessageEvent),
onDoubleTap: () =>
toolbarController?.showToolbar(context),
showToolbar(pangeaMessageEvent),
onLongPress: () =>
showToolbar(pangeaMessageEvent),
// onLongPress: longPressSelect
// ? null
// : () {
// HapticFeedback.heavyImpact();
// onSelect(event);
// },
// Pangea#
onLongPress: longPressSelect
? null
: () {
HapticFeedback.heavyImpact();
onSelect(event);
},
child: AnimatedOpacity(
opacity: animateIn
? 0
@ -346,13 +350,21 @@ class Message extends StatelessWidget {
),
// #Pangea
child: CompositedTransformTarget(
link: MatrixState.pAnyState
.layerLinkAndKey(event.eventId)
.link,
link: isOverlay
? LayerLinkAndKey('overlay_msg')
.link
: MatrixState.pAnyState
.layerLinkAndKey(event.eventId)
.link,
child: Container(
key: MatrixState.pAnyState
.layerLinkAndKey(event.eventId)
.key,
key: isOverlay
? LayerLinkAndKey('overlay_msg')
.key
: MatrixState.pAnyState
.layerLinkAndKey(
event.eventId,
)
.key,
// Pangea#
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
@ -439,8 +451,8 @@ class Message extends StatelessWidget {
pangeaMessageEvent:
pangeaMessageEvent,
immersionMode: immersionMode,
toolbarController:
toolbarController,
isOverlay: isOverlay,
controller: controller,
// Pangea#
),
if (event.hasAggregatedEvents(
@ -536,7 +548,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)
@ -587,7 +602,8 @@ class Message extends StatelessWidget {
children: [
if (pangeaMessageEvent?.showMessageButtons ?? false)
MessageButtons(
toolbarController: toolbarController,
controller: controller,
pangeaMessageEvent: pangeaMessageEvent!,
),
MessageReactions(event, timeline),
],
@ -631,6 +647,10 @@ class Message extends StatelessWidget {
container = row;
}
// #Pangea
container = Material(type: MaterialType.transparency, child: container);
// Pangea#
return Center(
child: Swipeable(
key: ValueKey(event.eventId),

View file

@ -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,
});
@ -301,48 +300,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) {
toolbarController!.controller.onSelectMessage(
pangeaMessageEvent!.event,
);
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)!),

View file

@ -1,11 +1,10 @@
import 'package:flutter/material.dart';
import 'package:emoji_proposal/emoji_proposal.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/app_emojis.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import '../../config/themes.dart';
class ReactionsPicker extends StatelessWidget {

View file

@ -1,11 +1,10 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pages/image_viewer/image_viewer_view.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import '../../utils/matrix_sdk_extensions/event_extension.dart';
class ImageViewer extends StatefulWidget {

View file

@ -61,11 +61,31 @@ class PangeaAnyState {
}
}
void closeAllOverlays() {
for (int i = 0; i < entries.length; i++) {
try {
entries.last.remove();
} catch (err, s) {
ErrorHandler.logError(
e: err,
s: s,
data: {
"overlay": entries.last,
},
);
}
entries.removeLast();
}
}
LayerLinkAndKey messageLinkAndKey(String eventId) => layerLinkAndKey(eventId);
// 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 {

View file

@ -11,6 +11,11 @@ import '../../config/themes.dart';
import '../../widgets/matrix.dart';
import 'error_handler.dart';
enum OverlayPositionEnum {
transform,
centered,
}
class OverlayUtil {
static showOverlay({
required BuildContext context,
@ -26,13 +31,13 @@ class OverlayUtil {
Alignment? targetAnchor,
Alignment? followerAnchor,
bool closePrevOverlay = true,
Function? onDismiss,
OverlayPositionEnum position = OverlayPositionEnum.transform,
}) {
try {
if (closePrevOverlay) {
MatrixState.pAnyState.closeOverlay();
}
final LayerLinkAndKey layerLinkAndKey =
MatrixState.pAnyState.layerLinkAndKey(transformTargetId);
final OverlayEntry entry = OverlayEntry(
builder: (context) => AnimatedContainer(
@ -43,18 +48,27 @@ class OverlayUtil {
if (backDropToDismiss)
TransparentBackdrop(
backgroundColor: backgroundColor,
onDismiss: onDismiss,
),
Positioned(
top: (position == OverlayPositionEnum.centered) ? 0 : null,
right: (position == OverlayPositionEnum.centered) ? 0 : null,
left: (position == OverlayPositionEnum.centered) ? 0 : null,
bottom: (position == OverlayPositionEnum.centered) ? 0 : null,
width: width,
height: height,
child: CompositedTransformFollower(
targetAnchor: targetAnchor ?? Alignment.topLeft,
followerAnchor: followerAnchor ?? Alignment.topLeft,
link: layerLinkAndKey.link,
showWhenUnlinked: false,
offset: offset ?? Offset.zero,
child: child,
),
child: (position != OverlayPositionEnum.transform)
? child
: CompositedTransformFollower(
targetAnchor: targetAnchor ?? Alignment.topLeft,
followerAnchor: followerAnchor ?? Alignment.topLeft,
link: MatrixState.pAnyState
.layerLinkAndKey(transformTargetId)
.link,
showWhenUnlinked: false,
offset: offset ?? Offset.zero,
child: child,
),
),
],
),
@ -191,8 +205,10 @@ class OverlayUtil {
class TransparentBackdrop extends StatelessWidget {
final Color? backgroundColor;
final Function? onDismiss;
const TransparentBackdrop({
super.key,
this.onDismiss,
this.backgroundColor,
});
@ -208,6 +224,9 @@ class TransparentBackdrop extends StatelessWidget {
focusColor: Colors.transparent,
highlightColor: Colors.transparent,
onTap: () {
if (onDismiss != null) {
onDismiss!();
}
MatrixState.pAnyState.closeOverlay();
},
child: Container(

View file

@ -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(

View file

@ -0,0 +1,223 @@
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/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/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
class MessageSelectionOverlay extends StatefulWidget {
final ChatController controller;
final Event event;
final PangeaMessageEvent pangeaMessageEvent;
final MessageMode? initialMode;
final MessageTextSelection textSelection;
const MessageSelectionOverlay({
required this.controller,
required this.event,
required this.pangeaMessageEvent,
required this.textSelection,
this.initialMode,
super.key,
});
@override
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 {
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) {
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 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: (_) => {},
onSelect: (_) => {},
immersionMode: widget.controller.choreographer.immersionMode,
controller: widget.controller,
timeline: widget.controller.timeline!,
isOverlay: true,
animateIn: false,
),
],
),
),
);
return Expanded(
child: Stack(
children: [
AnimatedPositioned(
duration: FluffyThemes.animationDuration,
left: 0,
right: 0,
bottom: adjustedOverlayBottomOffset == -1
? overlayBottomOffset
: adjustedOverlayBottomOffset,
child: Align(
alignment: Alignment.center,
child: overlayMessage,
),
),
Align(
alignment: Alignment.bottomCenter,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
OverlayFooter(controller: widget.controller),
],
),
),
Material(
child: OverlayHeader(controller: widget.controller),
),
],
),
);
}
}

View file

@ -1,233 +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/any_state_holder.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_speech_to_text_card.dart';
import 'package:fluffychat/pangea/widgets/chat/message_text_selection.dart';
import 'package:fluffychat/pangea/widgets/chat/message_translation_card.dart';
import 'package:fluffychat/pangea/widgets/chat/message_unsubscribed_card.dart';
import 'package:fluffychat/pangea/widgets/chat/overlay_message.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/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.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 setToolbar() {
toolbar ??= MessageToolbar(
textSelection: MessageTextSelection(),
room: pangeaMessageEvent.room,
toolbarModeStream: toolbarModeStream,
pangeaMessageEvent: pangeaMessageEvent,
immersionMode: immersionMode,
controller: controller,
);
}
void showToolbar(BuildContext context, {MessageMode? mode}) {
// Don't show toolbar if keyboard open
if (controller.inputFocus.hasFocus) {
FocusManager.instance.primaryFocus?.unfocus();
return;
}
bool toolbarUp = true;
if (highlighted) return;
if (controller.selectMode) {
controller.clearSelectedEvents();
}
if (!MatrixState.pangeaController.languageController.languagesSet) {
pLanguageDialog(context, () {});
return;
}
focusNode.requestFocus();
final LayerLinkAndKey layerLinkAndKey =
MatrixState.pAnyState.layerLinkAndKey(targetId);
final targetRenderBox =
layerLinkAndKey.key.currentContext?.findRenderObject();
if (targetRenderBox != null) {
final Size transformTargetSize = (targetRenderBox as RenderBox).size;
messageWidth = transformTargetSize.width;
final Offset targetOffset = (targetRenderBox).localToGlobal(Offset.zero);
// If there is enough space above, procede as normal
// Else if there is enough space below, show toolbar underneath
if (targetOffset.dy < 320) {
final spaceBeneath = MediaQuery.of(context).size.height -
(targetOffset.dy + transformTargetSize.height);
if (spaceBeneath >= 320) {
toolbarUp = false;
}
// See if it's possible to scroll up to make space
else if (controller.scrollController.offset - targetOffset.dy + 320 >=
controller.scrollController.position.minScrollExtent &&
controller.scrollController.offset - targetOffset.dy + 320 <=
controller.scrollController.position.maxScrollExtent) {
controller.scrollController.animateTo(
controller.scrollController.offset - targetOffset.dy + 320,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
);
}
// See if it's possible to scroll down to make space
else if (controller.scrollController.offset + spaceBeneath - 320 >=
controller.scrollController.position.minScrollExtent &&
controller.scrollController.offset + spaceBeneath - 320 <=
controller.scrollController.position.maxScrollExtent) {
controller.scrollController.animateTo(
controller.scrollController.offset + spaceBeneath - 320,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
);
toolbarUp = false;
}
// If message is too big and can't scroll either way
// Scroll up as much as possible, and show toolbar above
else {
controller.scrollController.animateTo(
controller.scrollController.position.minScrollExtent,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
);
}
}
}
final Widget overlayMessage = OverlayMessage(
pangeaMessageEvent.event,
timeline: pangeaMessageEvent.timeline,
immersionMode: immersionMode,
ownMessage: pangeaMessageEvent.ownMessage,
toolbarController: this,
width: messageWidth,
nextEvent: nextEvent,
previousEvent: previousEvent,
);
// 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 = Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: pangeaMessageEvent.ownMessage
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [
toolbarUp ? toolbar! : overlayMessage,
const SizedBox(height: 6),
toolbarUp ? overlayMessage : toolbar!,
],
);
} catch (err) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: err, s: StackTrace.current);
return;
}
OverlayUtil.showOverlay(
context: context,
child: overlayEntry,
transformTargetId: targetId,
targetAnchor: pangeaMessageEvent.ownMessage
? toolbarUp
? Alignment.bottomRight
: Alignment.topRight
: toolbarUp
? Alignment.bottomLeft
: Alignment.topLeft,
followerAnchor: pangeaMessageEvent.ownMessage
? toolbarUp
? Alignment.bottomRight
: Alignment.topRight
: toolbarUp
? Alignment.bottomLeft
: Alignment.topLeft,
backgroundColor: const Color.fromRGBO(0, 0, 0, 1).withAlpha(100),
closePrevOverlay:
MatrixState.pangeaController.subscriptionController.isSubscribed,
);
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
@ -239,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.
@ -278,7 +79,7 @@ class MessageToolbarState extends State<MessageToolbar> {
toolbarContent = MessageUnsubscribedCard(
languageTool: newMode.title(context),
mode: newMode,
toolbarModeStream: widget.toolbarModeStream,
controller: this,
);
} else {
switch (currentMode) {
@ -317,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,
);
}
@ -350,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,
);
}
@ -364,30 +165,25 @@ class MessageToolbarState extends State<MessageToolbar> {
void spellCheck() {}
void showMore() {
MatrixState.pAnyState.closeOverlay();
widget.controller.onSelectMessage(widget.pangeaMessageEvent.event);
}
@override
void initState() {
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;
@ -410,13 +206,37 @@ class MessageToolbarState extends State<MessageToolbar> {
@override
void dispose() {
selectionStream.cancel();
toolbarModeStream.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
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),
@ -430,72 +250,26 @@ class MessageToolbarState extends State<MessageToolbar> {
Radius.circular(25),
),
),
constraints: const BoxConstraints(
maxWidth: 300,
minWidth: 300,
maxHeight: 300,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
constraints: const BoxConstraints(
minWidth: 300,
maxHeight: 228,
),
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() +
[
Tooltip(
message: L10n.of(context)!.more,
child: IconButton(
icon: const Icon(Icons.add_reaction_outlined),
onPressed: showMore,
),
),
],
),
buttonRow,
],
),
),

View file

@ -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),
),
),

View file

@ -0,0 +1,48 @@
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:flutter/material.dart';
class OverlayFooter extends StatelessWidget {
final ChatController controller;
const OverlayFooter({
required this.controller,
super.key,
});
@override
Widget build(BuildContext context) {
final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0;
return Container(
margin: EdgeInsets.only(
bottom: bottomSheetPadding,
left: bottomSheetPadding,
right: bottomSheetPadding,
),
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 2.5,
),
alignment: Alignment.center,
child: Column(
children: [
Material(
clipBehavior: Clip.hardEdge,
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: const BorderRadius.all(
Radius.circular(24),
),
child: Column(
children: [
ReactionsPicker(controller),
ChatInputRow(controller),
],
),
),
],
),
);
}
}

View file

@ -0,0 +1,88 @@
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/chat_app_bar_title.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
class OverlayHeader extends StatelessWidget {
final ChatController controller;
const OverlayHeader({
required this.controller,
super.key,
});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
AppBar(
actionsIconTheme: IconThemeData(
color: Theme.of(context).colorScheme.primary,
),
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: controller.clearSelectedEvents,
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 (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,
),
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,
),
],
),
],
);
}
}

View file

@ -1,204 +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:flutter/material.dart';
import 'package:matrix/matrix.dart';
import '../../../config/app_config.dart';
class OverlayMessage extends StatelessWidget {
final Event event;
final Event? nextEvent;
final Event? previousEvent;
final bool selected;
final Timeline timeline;
// final LanguageModel? selectedDisplayLang;
final bool immersionMode;
// final bool definitions;
final bool ownMessage;
final ToolbarDisplayController toolbarController;
final double? width;
const OverlayMessage(
this.event, {
this.nextEvent,
this.previousEvent,
this.selected = false,
required this.timeline,
required this.immersionMode,
required this.ownMessage,
required this.toolbarController,
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.surfaceContainerHighest;
final isLight = Theme.of(context).brightness == Brightness.light;
var lightness = isLight ? .05 : .85;
final textColor = ownMessage
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.onSurface;
const hardCorner = Radius.circular(4);
final displayTime = event.type == EventTypes.RoomCreate ||
nextEvent == null ||
!event.originServerTs.sameEnvironment(nextEvent!.originServerTs);
final nextEventSameSender = nextEvent != null &&
{
EventTypes.Message,
EventTypes.Sticker,
EventTypes.Encrypted,
}.contains(nextEvent!.type) &&
nextEvent!.senderId == event.senderId &&
!displayTime;
final previousEventSameSender = previousEvent != null &&
{
EventTypes.Message,
EventTypes.Sticker,
EventTypes.Encrypted,
}.contains(previousEvent!.type) &&
previousEvent!.senderId == event.senderId &&
previousEvent!.originServerTs.sameEnvironment(event.originServerTs);
const roundedCorner = Radius.circular(AppConfig.borderRadius);
final borderRadius = BorderRadius.only(
topLeft: !ownMessage && nextEventSameSender ? hardCorner : roundedCorner,
topRight: ownMessage && nextEventSameSender ? hardCorner : roundedCorner,
bottomLeft:
!ownMessage && previousEventSameSender ? hardCorner : roundedCorner,
bottomRight:
ownMessage && previousEventSameSender ? 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
? (color.red + lightness * (255 - color.red)).round()
: (color.red * lightness).round(),
isLight
? (color.green + lightness * (255 - color.green)).round()
: (color.green * lightness).round(),
isLight
? (color.blue + lightness * (255 - color.blue)).round()
: (color.blue * lightness).round(),
);
final pangeaMessageEvent = PangeaMessageEvent(
event: event,
timeline: timeline,
ownMessage: ownMessage,
);
return Flexible(
child: 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,
),
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,
),
),
],
],
),
),
],
),
),
),
);
}
}

View file

@ -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,
),
);
),
);
}
}
}

View file

@ -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,38 +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)) {
widget.toolbarController?.controller.onSelectMessage(
widget.pangeaMessageEvent.event,
);
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,