toolbar working

pull/1011/head
ggurdin 2 years ago
parent 57957eac9e
commit 2cb77732e3

@ -3874,5 +3874,6 @@
"type": "text",
"placeholders": {}
},
"showDefinition": "Show Definition"
"define": "Define",
"listen": "Listen"
}

@ -3,7 +3,7 @@ import 'dart:developer';
import 'dart:io';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:desktop_drop/desktop_drop.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';
@ -20,14 +20,17 @@ import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/models/choreo_record.dart';
import 'package:fluffychat/pangea/models/class_model.dart';
import 'package:fluffychat/pangea/models/message_data_models.dart';
import 'package:fluffychat/pangea/models/pangea_message_event.dart';
import 'package:fluffychat/pangea/models/student_analytics_summary_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
import 'package:fluffychat/pangea/utils/report_message.dart';
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
import 'package:fluffychat/pangea/widgets/igc/pangea_text_controller.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.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';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/app_lock.dart';
@ -144,45 +147,45 @@ class ChatController extends State<ChatPageWithRoom>
Timer? typingCoolDown;
Timer? typingTimeout;
bool currentlyTyping = false;
bool dragging = false;
void onDragEntered(_) => setState(() => dragging = true);
void onDragExited(_) => setState(() => dragging = false);
void onDragDone(DropDoneDetails details) async {
setState(() => dragging = false);
final bytesList = await showFutureLoadingDialog(
context: context,
future: () => Future.wait(
details.files.map(
(xfile) => xfile.readAsBytes(),
),
),
);
if (bytesList.error != null) return;
final matrixFiles = <MatrixFile>[];
for (var i = 0; i < bytesList.result!.length; i++) {
matrixFiles.add(
MatrixFile(
bytes: bytesList.result![i],
name: details.files[i].name,
).detectFileType,
);
}
// #Pangea
if (matrixFiles.isEmpty) return;
// Pangea#
await showAdaptiveDialog(
context: context,
builder: (c) => SendFileDialog(
files: matrixFiles,
room: room,
),
);
}
// #Pangea
// bool dragging = false;
// void onDragEntered(_) => setState(() => dragging = true);
// void onDragExited(_) => setState(() => dragging = false);
// void onDragDone(DropDoneDetails details) async {
// setState(() => dragging = false);
// final bytesList = await showFutureLoadingDialog(
// context: context,
// future: () => Future.wait(
// details.files.map(
// (xfile) => xfile.readAsBytes(),
// ),
// ),
// );
// if (bytesList.error != null) return;
// final matrixFiles = <MatrixFile>[];
// for (var i = 0; i < bytesList.result!.length; i++) {
// matrixFiles.add(
// MatrixFile(
// bytes: bytesList.result![i],
// name: details.files[i].name,
// ).detectFileType,
// );
// }
// if (matrixFiles.isEmpty) return;
// await showAdaptiveDialog(
// context: context,
// builder: (c) => SendFileDialog(
// files: matrixFiles,
// room: room,
// ),
// );
// }
// Pangea#
bool get canSaveSelectedEvent =>
selectedEvents.length == 1 &&
@ -1542,7 +1545,51 @@ class ChatController extends State<ChatPageWithRoom>
lastState = currentState;
return currentState;
}
// #Pangea
List<Event> get events =>
timeline!.events.where((event) => event.isVisibleInGui).toList();
final Map<String, ToolbarDisplayController> _messageToolbarControllers = {};
final Map<String, PangeaMessageEvent> _pangeaMessageEvents = {};
PangeaMessageEvent? pangeaMessageEvent(String eventId) {
final Event? event =
events.firstWhereOrNull((event) => event.eventId == eventId);
if (timeline == null || event == null || event.type != EventTypes.Message) {
return null;
}
if (!_pangeaMessageEvents.containsKey(eventId)) {
_pangeaMessageEvents[eventId] = PangeaMessageEvent(
event: event,
timeline: timeline!,
ownMessage: event.senderId == Matrix.of(context).client.userID,
);
}
return _pangeaMessageEvents[eventId];
}
ToolbarDisplayController? messageToolbarController(String eventId) {
final Event? event =
events.firstWhereOrNull((event) => event.eventId == eventId);
if (timeline == null || event == null || event.type != EventTypes.Message) {
return null;
}
final PangeaMessageEvent? messageEvent = pangeaMessageEvent(eventId);
if (messageEvent == null) return null;
if (!_messageToolbarControllers.containsKey(event.eventId)) {
_messageToolbarControllers[event.eventId] = ToolbarDisplayController(
targetId: event.eventId,
pangeaMessageEvent: messageEvent,
immersionMode: choreographer.immersionMode,
controller: this,
);
_messageToolbarControllers[event.eventId]!.setToolbar();
}
return _messageToolbarControllers[eventId];
}
// Pangea#
@override
Widget build(BuildContext context) => ChatView(this);

@ -24,9 +24,12 @@ class ChatEventList extends StatelessWidget {
Widget build(BuildContext context) {
final horizontalPadding = FluffyThemes.isColumnMode(context) ? 8.0 : 0.0;
final events = controller.timeline!.events
.where((event) => event.isVisibleInGui)
.toList();
// #Pangea
// final events = controller.timeline!.events
// .where((event) => event.isVisibleInGui)
// .toList();
final events = controller.events;
// Pangea#
final animateInEventIndex = controller.animateInEventIndex;
// create a map of eventId --> index to greatly improve performance of
@ -153,6 +156,7 @@ class ChatEventList extends StatelessWidget {
controller.choreographer.messageOptions.selectedDisplayLang,
immersionMode: controller.choreographer.immersionMode,
definitions: controller.choreographer.definitionsEnabled,
controller: controller,
// Pangea#
selected: controller.selectedEvents
.any((e) => e.eventId == event.eventId),

@ -58,21 +58,18 @@ class ChatInputRow extends StatelessWidget {
),
)
else
// #Pangea
PangeaMessageActions(chatController: controller),
// SizedBox(
// height: 56,
// child: TextButton(
// onPressed: controller.forwardEventsAction,
// child: Row(
// children: <Widget>[
// const Icon(Icons.keyboard_arrow_left_outlined),
// Text(L10n.of(context)!.forward),
// ],
// ),
// ),
// ),
// Pangea#
SizedBox(
height: 56,
child: TextButton(
onPressed: controller.forwardEventsAction,
child: Row(
children: <Widget>[
const Icon(Icons.keyboard_arrow_left_outlined),
Text(L10n.of(context)!.forward),
],
),
),
),
controller.selectedEvents.length == 1
? controller.selectedEvents.first
.getDisplayEvent(controller.timeline!)

@ -1,5 +1,4 @@
import 'package:badges/badges.dart';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/chat.dart';
@ -26,10 +25,7 @@ import '../../utils/stream_extension.dart';
import 'chat_emoji_picker.dart';
import 'chat_input_row.dart';
//#Pangea
// enum _EventContextAction { info, report }
enum _EventContextAction { info, forward, report }
//Pangea#
enum _EventContextAction { info, report }
class ChatView extends StatelessWidget {
final ChatController controller;
@ -39,9 +35,6 @@ class ChatView extends StatelessWidget {
List<Widget> _appBarActions(BuildContext context) {
if (controller.selectMode) {
return [
// #Pangea
LanguageDisplayToggle(controller: controller),
// Pangea#
if (controller.canEditSelectedEvents)
IconButton(
icon: const Icon(Icons.edit_outlined),
@ -85,11 +78,6 @@ class ChatView extends StatelessWidget {
case _EventContextAction.report:
controller.reportEventAction();
break;
// #Pangea
case _EventContextAction.forward:
controller.forwardEventsAction();
break;
// Pangea#
}
},
itemBuilder: (context) => [
@ -105,17 +93,6 @@ class ChatView extends StatelessWidget {
// ],
// ),
// ),
PopupMenuItem(
value: _EventContextAction.forward,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.forward),
const SizedBox(width: 12),
Text(L10n.of(context)!.forward),
],
),
),
// Pangea#
if (controller.selectedEvents.single.status.isSent)
PopupMenuItem(
@ -248,199 +225,199 @@ class ChatView extends StatelessWidget {
: null)
// #Pangea
: null,
body: DropTarget(
onDragDone: controller.onDragDone,
onDragEntered: controller.onDragEntered,
onDragExited: controller.onDragExited,
child: Stack(
children: <Widget>[
SafeArea(
child: Column(
children: <Widget>[
TombstoneDisplay(controller),
if (scrollUpBannerEventId != null)
Material(
color: Theme.of(context)
.colorScheme
.surfaceVariant,
shape: Border(
bottom: BorderSide(
width: 1,
color: Theme.of(context).dividerColor,
),
),
child: ListTile(
leading: IconButton(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
icon: const Icon(Icons.close),
tooltip: L10n.of(context)!.close,
onPressed: () {
controller
.discardScrollUpBannerEventId();
controller.setReadMarker();
},
),
title: Text(
L10n.of(context)!.jumpToLastReadMessage,
),
contentPadding:
const EdgeInsets.only(left: 8),
trailing: TextButton(
onPressed: () {
controller.scrollToEventId(
scrollUpBannerEventId,
);
controller
.discardScrollUpBannerEventId();
},
child: Text(L10n.of(context)!.jump),
),
body:
// #Pangea
// DropTarget(
// onDragDone: controller.onDragDone,
// onDragEntered: controller.onDragEntered,
// onDragExited: controller.onDragExited,
// child:
// Pangea#
Stack(
children: <Widget>[
SafeArea(
child: Column(
children: <Widget>[
TombstoneDisplay(controller),
if (scrollUpBannerEventId != null)
Material(
color: Theme.of(context)
.colorScheme
.surfaceVariant,
shape: Border(
bottom: BorderSide(
width: 1,
color: Theme.of(context).dividerColor,
),
),
PinnedEvents(controller),
Expanded(
child: GestureDetector(
onTap: controller.clearSingleSelectedEvent,
child: Builder(
builder: (context) {
if (controller.timeline == null) {
return const Center(
child: CircularProgressIndicator
.adaptive(
strokeWidth: 2,
),
);
}
return ChatEventList(
controller: controller,
child: ListTile(
leading: IconButton(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
icon: const Icon(Icons.close),
tooltip: L10n.of(context)!.close,
onPressed: () {
controller.discardScrollUpBannerEventId();
controller.setReadMarker();
},
),
title: Text(
L10n.of(context)!.jumpToLastReadMessage,
),
contentPadding:
const EdgeInsets.only(left: 8),
trailing: TextButton(
onPressed: () {
controller.scrollToEventId(
scrollUpBannerEventId,
);
controller.discardScrollUpBannerEventId();
},
child: Text(L10n.of(context)!.jump),
),
),
),
if (controller.room.canSendDefaultMessages &&
controller.room.membership == Membership.join)
// #Pangea
// Container(
ConditionalFlexible(
isScroll: controller.isRowScrollable,
child: ConditionalScroll(
isScroll: controller.isRowScrollable,
child: MeasurableWidget(
onChange: (size, position) {
controller.inputRowSize = size!.height;
},
child: Container(
// Pangea#
margin: EdgeInsets.only(
bottom: bottomSheetPadding,
left: bottomSheetPadding,
right: bottomSheetPadding,
PinnedEvents(controller),
Expanded(
child: GestureDetector(
onTap: controller.clearSingleSelectedEvent,
child: Builder(
builder: (context) {
if (controller.timeline == null) {
return const Center(
child:
CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
constraints: const BoxConstraints(
maxWidth:
FluffyThemes.columnWidth * 2.5,
),
alignment: Alignment.center,
child: Material(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(
AppConfig.borderRadius,
),
bottomRight: Radius.circular(
AppConfig.borderRadius,
),
);
}
return ChatEventList(
controller: controller,
);
},
),
),
),
if (controller.room.canSendDefaultMessages &&
controller.room.membership == Membership.join)
// #Pangea
// Container(
ConditionalFlexible(
isScroll: controller.isRowScrollable,
child: ConditionalScroll(
isScroll: controller.isRowScrollable,
child: MeasurableWidget(
onChange: (size, position) {
controller.inputRowSize = size!.height;
},
child: Container(
// Pangea#
margin: EdgeInsets.only(
bottom: bottomSheetPadding,
left: bottomSheetPadding,
right: bottomSheetPadding,
),
constraints: const BoxConstraints(
maxWidth:
FluffyThemes.columnWidth * 2.5,
),
alignment: Alignment.center,
child: Material(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(
AppConfig.borderRadius,
),
elevation: 4,
shadowColor:
Colors.black.withAlpha(64),
clipBehavior: Clip.hardEdge,
color: Theme.of(context).brightness ==
Brightness.light
? Colors.white
: Colors.black,
child: controller
.room.isAbandonedDMRoom ==
true
? Row(
mainAxisAlignment:
MainAxisAlignment
.spaceEvenly,
children: [
TextButton.icon(
style:
TextButton.styleFrom(
padding:
const EdgeInsets
.all(16),
foregroundColor:
Theme.of(context)
.colorScheme
.error,
),
icon: const Icon(
Icons.archive_outlined,
),
onPressed:
controller.leaveChat,
label: Text(
L10n.of(context)!.leave,
),
bottomRight: Radius.circular(
AppConfig.borderRadius,
),
),
elevation: 4,
shadowColor: Colors.black.withAlpha(64),
clipBehavior: Clip.hardEdge,
color: Theme.of(context).brightness ==
Brightness.light
? Colors.white
: Colors.black,
child: controller
.room.isAbandonedDMRoom ==
true
? Row(
mainAxisAlignment:
MainAxisAlignment
.spaceEvenly,
children: [
TextButton.icon(
style: TextButton.styleFrom(
padding:
const EdgeInsets.all(
16),
foregroundColor:
Theme.of(context)
.colorScheme
.error,
),
TextButton.icon(
style:
TextButton.styleFrom(
padding:
const EdgeInsets
.all(16),
),
icon: const Icon(
Icons.forum_outlined,
),
onPressed: controller
.recreateChat,
label: Text(
L10n.of(context)!
.reopenChat,
),
icon: const Icon(
Icons.archive_outlined,
),
],
)
: Column(
mainAxisSize:
MainAxisSize.min,
children: [
const ConnectionStatusHeader(),
ReactionsPicker(controller),
ReplyDisplay(controller),
ChatInputRow(controller),
ChatEmojiPicker(controller),
],
),
),
onPressed:
controller.leaveChat,
label: Text(
L10n.of(context)!.leave,
),
),
TextButton.icon(
style: TextButton.styleFrom(
padding:
const EdgeInsets.all(
16),
),
icon: const Icon(
Icons.forum_outlined,
),
onPressed:
controller.recreateChat,
label: Text(
L10n.of(context)!
.reopenChat,
),
),
],
)
: Column(
mainAxisSize: MainAxisSize.min,
children: [
const ConnectionStatusHeader(),
ReactionsPicker(controller),
ReplyDisplay(controller),
ChatInputRow(controller),
ChatEmojiPicker(controller),
],
),
),
),
),
),
if (controller.dragging)
Container(
color: Theme.of(context)
.scaffoldBackgroundColor
.withOpacity(0.9),
alignment: Alignment.center,
child: const Icon(
Icons.upload_outlined,
size: 100,
),
),
],
),
),
// #Pangea
// if (controller.dragging)
// Container(
// color: Theme.of(context)
// .scaffoldBackgroundColor
// .withOpacity(0.9),
// alignment: Alignment.center,
// child: const Icon(
// Icons.upload_outlined,
// size: 100,
// ),
// ),
// Pangea#
],
),
],
),
),
],
),
// ),
);
},
),
@ -481,4 +458,4 @@ class ConditionalScroll extends StatelessWidget {
return child;
}
}
// #Pangea
// #Pangea

@ -1,10 +1,8 @@
import 'package:collection/collection.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/utils/show_defintion_util.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/mxc_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_highlighter/flutter_highlighter.dart';
import 'package:flutter_highlighter/themes/shades-of-purple.dart';
import 'package:flutter_html/flutter_html.dart';
@ -21,7 +19,7 @@ class HtmlMessage extends StatelessWidget {
final Room room;
final Color textColor;
// #Pangea
final ShowDefintionUtil? messageToolbar;
// final ShowDefintionUtil? messageToolbar;
// Pangea#
const HtmlMessage({
@ -30,7 +28,7 @@ class HtmlMessage extends StatelessWidget {
required this.room,
this.textColor = Colors.black,
// #Pangea
this.messageToolbar,
// this.messageToolbar,
// Pangea#
});
@ -101,20 +99,20 @@ class HtmlMessage extends StatelessWidget {
// there is no need to pre-validate the html, as we validate it while rendering
// #Pangea
return MouseRegion(
onHover: messageToolbar?.onMouseRegionUpdate,
// onHover: messageToolbar?.onMouseRegionUpdate,
child: SelectionArea(
onSelectionChanged: (SelectedContent? selection) =>
messageToolbar?.onTextSelection(
selectedContent: selection,
context: context,
),
focusNode: messageToolbar?.focusNode,
contextMenuBuilder: (context, state) =>
messageToolbar?.contextMenuOverride(
context: context,
contentSelection: state,
) ??
const SizedBox(),
// onSelectionChanged: (SelectedContent? selection) =>
// messageToolbar?.onTextSelection(
// selectedContent: selection,
// context: context,
// ),
// focusNode: messageToolbar?.focusNode,
// contextMenuBuilder: (context, state) =>
// messageToolbar?.contextMenuOverride(
// context: context,
// contentSelection: state,
// ) ??
// const SizedBox(),
// Pangea#
child: Html.fromElement(
documentElement: element as dom.Element,

@ -1,7 +1,9 @@
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/models/language_model.dart';
import 'package:fluffychat/pangea/models/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:fluffychat/utils/string_color.dart';
@ -9,6 +11,7 @@ import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/hover_builder.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';
@ -40,6 +43,7 @@ class Message extends StatelessWidget {
final LanguageModel? selectedDisplayLang;
final bool immersionMode;
final bool definitions;
final ChatController controller;
// Pangea#
const Message(
@ -61,6 +65,7 @@ class Message extends StatelessWidget {
required this.selectedDisplayLang,
required this.immersionMode,
required this.definitions,
required this.controller,
// Pangea#
super.key,
});
@ -138,12 +143,10 @@ class Message extends StatelessWidget {
}
// #Pangea
final pangeaMessageEvent = PangeaMessageEvent(
event: event,
timeline: timeline,
ownMessage: ownMessage,
selected: selected,
);
final PangeaMessageEvent? pangeaMessageEvent =
controller.pangeaMessageEvent(event.eventId);
final ToolbarDisplayController? toolbarController =
controller.messageToolbarController(event.eventId);
// Pangea#
final resetAnimateIn = this.resetAnimateIn;
@ -241,25 +244,13 @@ class Message extends StatelessWidget {
alignment: alignment,
padding: const EdgeInsets.only(left: 8),
child: GestureDetector(
onTap: () => print("got message tap"),
onDoubleTap: () => print("got message double tap"),
onDoubleTapDown: (details) =>
print("got message double tap down"),
onLongPress: longPressSelect
? selected
? null
: () => print('long press')
: () {
onSelect(event);
// Android usually has a vibration effect on long press:
if (PlatformInfos.isAndroid) {
Vibration.hasVibrator().then((has) {
if (has == true) {
Vibration.vibrate(duration: 50);
}
});
}
},
onTap: () => toolbarController?.showToolbar(context),
onDoubleTap: () =>
toolbarController?.showToolbar(context),
onLongPress: () {
onSelect(event);
HapticFeedback.selectionClick();
},
child: AnimatedOpacity(
opacity: animateIn
? 0
@ -356,9 +347,8 @@ class Message extends StatelessWidget {
// #Pangea
selected: selected,
pangeaMessageEvent: pangeaMessageEvent,
selectedDisplayLang: selectedDisplayLang,
immersionMode: immersionMode,
definitions: definitions,
toolbarController: toolbarController,
// Pangea#
),
if (event.hasAggregatedEvents(
@ -366,7 +356,8 @@ class Message extends StatelessWidget {
RelationshipTypes.edit,
) // #Pangea
||
(pangeaMessageEvent.showUseType)
(pangeaMessageEvent?.showUseType ??
false)
// Pangea#
)
Padding(
@ -378,8 +369,9 @@ class Message extends StatelessWidget {
children: [
// #Pangea
if (pangeaMessageEvent
.showUseType) ...[
pangeaMessageEvent.useType
?.showUseType ??
false) ...[
pangeaMessageEvent!.useType
.iconView(
context,
textColor.withAlpha(164),

@ -2,7 +2,9 @@ import 'package:fluffychat/pages/chat/events/html_message.dart';
import 'package:fluffychat/pages/chat/events/video_player.dart';
import 'package:fluffychat/pangea/models/language_model.dart';
import 'package:fluffychat/pangea/models/pangea_message_event.dart';
import 'package:fluffychat/pangea/utils/show_defintion_util.dart';
import 'package:fluffychat/pangea/widgets/chat/message_context_menu.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/igc/pangea_rich_text.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
@ -31,27 +33,24 @@ class MessageContent extends StatelessWidget {
final BorderRadius borderRadius;
// #Pangea
final bool selected;
final PangeaMessageEvent pangeaMessageEvent;
final PangeaMessageEvent? pangeaMessageEvent;
//question: are there any performance benefits to using booleans
//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 LanguageModel? selectedDisplayLang;
final bool immersionMode;
final bool definitions;
ShowDefintionUtil? messageToolbar;
final ToolbarDisplayController? toolbarController;
// Pangea#
MessageContent(
const MessageContent(
this.event, {
this.onInfoTab,
super.key,
required this.textColor,
// #Pangea
required this.selected,
required this.pangeaMessageEvent,
required this.selectedDisplayLang,
this.pangeaMessageEvent,
required this.immersionMode,
required this.definitions,
required this.toolbarController,
// Pangea#
required this.borderRadius,
});
@ -124,18 +123,6 @@ class MessageContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
// #Pangea
messageToolbar = ShowDefintionUtil(
targetId: pangeaMessageEvent.eventId,
room: pangeaMessageEvent.room,
langCode: selectedDisplayLang?.langCode ??
MatrixState.pangeaController.languageController.activeL2Code(
roomID: pangeaMessageEvent.room.id,
) ??
LanguageModel.unknown.langCode,
messageText: "",
);
// Pangea#
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
final buttonTextColor = textColor;
switch (event.type) {
@ -186,7 +173,7 @@ class MessageContent extends StatelessWidget {
event.isRichMessage
// #Pangea
&&
!pangeaMessageEvent.showRichText
!(pangeaMessageEvent?.showRichText ?? false)
// Pangea#
) {
var html = event.formattedText;
@ -194,13 +181,13 @@ class MessageContent extends StatelessWidget {
html = '* $html';
}
// #Pangea
messageToolbar?.messageText = html;
// messageToolbar?.messageText = html;
// Pangea#
return HtmlMessage(
html: html,
textColor: textColor,
room: event.room,
messageToolbar: messageToolbar,
// messageToolbar: messageToolbar,
);
}
// else we fall through to the normal message rendering
@ -286,85 +273,86 @@ class MessageContent extends StatelessWidget {
decoration: event.redacted ? TextDecoration.lineThrough : null,
height: 1.3,
);
if (pangeaMessageEvent.showRichText) {
return MouseRegion(
onHover: messageToolbar?.onMouseRegionUpdate,
child: PangeaRichText(
style: messageTextStyle,
selected: selected,
pangeaMessageEvent: pangeaMessageEvent,
immersionMode: immersionMode,
definitions: definitions,
selectedDisplayLang: selectedDisplayLang,
messageToolbar: messageToolbar,
),
if (pangeaMessageEvent?.showRichText ?? false) {
return PangeaRichText(
style: messageTextStyle,
pangeaMessageEvent: pangeaMessageEvent!,
immersionMode: immersionMode,
toolbarController: toolbarController!,
// selectedDisplayLang: selectedDisplayLang,
// highlighted: toolbarController!.highlighted,
);
}
return MouseRegion(
onHover: messageToolbar?.onMouseRegionUpdate,
child: FutureBuilder<String>(
// Pangea#
future: event.calcLocalizedBody(
MatrixLocals(L10n.of(context)!),
hideReply: true,
),
builder: (context, snapshot) {
// #Pangea
if (!snapshot.hasData) {
return Text(
event.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!),
hideReply: true,
),
style: messageTextStyle,
);
}
// return Linkify(
final String messageText = snapshot.data ??
event.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!),
hideReply: true,
);
messageToolbar?.messageText = messageText;
return SelectableLinkify(
// Pangea#
text: messageText,
focusNode: messageToolbar?.focusNode,
contextMenuBuilder: (context, state) =>
messageToolbar?.contextMenuOverride(
context: context,
textSelection: state,
) ??
const SizedBox(),
// text: snapshot.data ??
// event.calcLocalizedBodyFallback(
// MatrixLocals(L10n.of(context)!),
// hideReply: true,
// ),
style: TextStyle(
color: textColor,
fontSize: bigEmotes ? fontSize * 3 : fontSize,
decoration:
event.redacted ? TextDecoration.lineThrough : null,
return FutureBuilder<String>(
// Pangea#
future: event.calcLocalizedBody(
MatrixLocals(L10n.of(context)!),
hideReply: true,
),
builder: (context, snapshot) {
// #Pangea
if (!snapshot.hasData) {
return Text(
event.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!),
hideReply: true,
),
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: textColor.withAlpha(150),
fontSize: bigEmotes ? fontSize * 3 : fontSize,
decoration: TextDecoration.underline,
decorationColor: textColor.withAlpha(150),
style: messageTextStyle,
);
}
// return Linkify(
final String messageText = snapshot.data ??
event.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!),
hideReply: true,
);
toolbarController?.toolbar?.textSelection.setMessageText(
messageText,
);
return SelectableLinkify(
onSelectionChanged: (selection, cause) => toolbarController
?.toolbar?.textSelection
.onTextSelection(selection),
onTap: () => toolbarController?.showToolbar(context),
// Pangea#
text: toolbarController?.toolbar?.textSelection.messageText ??
messageText,
focusNode: toolbarController?.focusNode,
contextMenuBuilder: (context, state) =>
MessageContextMenu.contextMenuOverride(
context: context,
textSelection: state,
onDefine: () => toolbarController?.showToolbar(
context,
mode: MessageMode.definition,
),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
onSelectionChanged: (selection, cause) =>
messageToolbar?.onTextSelection(
selectedText: selection,
cause: cause,
context: context,
onListen: () => toolbarController?.showToolbar(
context,
mode: MessageMode.play,
),
onTap: () => messageToolbar?.onTextTap(context),
);
},
),
),
// text: snapshot.data ??
// event.calcLocalizedBodyFallback(
// MatrixLocals(L10n.of(context)!),
// hideReply: true,
// ),
style: TextStyle(
color: textColor,
fontSize: bigEmotes ? fontSize * 3 : fontSize,
decoration:
event.redacted ? TextDecoration.lineThrough : null,
),
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: textColor.withAlpha(150),
fontSize: bigEmotes ? fontSize * 3 : fontSize,
decoration: TextDecoration.underline,
decorationColor: textColor.withAlpha(150),
),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
// onTap: () => messageToolbar?.onTextTap(context),
);
},
);
}
case EventTypes.CallInvite:

@ -269,6 +269,7 @@ class ITController {
completedITSteps.add(itStep);
showChoiceFeedback = true;
Future.delayed(
const Duration(
milliseconds: ChoreoConstants.millisecondsToDisplayFeedback,

@ -224,39 +224,40 @@ class ITChoices extends StatelessWidget {
int index, [
Color? borderColor,
String? choiceFeedback,
]) =>
OverlayUtil.showPositionedCard(
context: context,
cardToShow: choiceFeedback == null
? WordDataCard(
word: controller.currentITStep!.continuances[index].text,
wordLang: controller.targetLangCode,
fullText: sourceText ?? controller.choreographer.currentText,
fullTextLang: sourceText != null
? controller.sourceLangCode
: controller.targetLangCode,
hasInfo: controller.currentITStep!.continuances[index].hasInfo,
choiceFeedback: choiceFeedback,
room: controller.choreographer.chatController.room,
)
: ITFeedbackCard(
req: ITFeedbackRequestModel(
sourceText: sourceText!,
currentText: controller.choreographer.currentText,
chosenContinuance:
controller.currentITStep!.continuances[index].text,
bestContinuance: controller.currentITStep!.best.text,
feedbackLang: controller.targetLangCode,
sourceTextLang: controller.sourceLangCode,
targetLang: controller.targetLangCode,
),
choiceFeedback: choiceFeedback,
]) {
OverlayUtil.showPositionedCard(
context: context,
cardToShow: choiceFeedback == null
? WordDataCard(
word: controller.currentITStep!.continuances[index].text,
wordLang: controller.targetLangCode,
fullText: sourceText ?? controller.choreographer.currentText,
fullTextLang: sourceText != null
? controller.sourceLangCode
: controller.targetLangCode,
hasInfo: controller.currentITStep!.continuances[index].hasInfo,
choiceFeedback: choiceFeedback,
room: controller.choreographer.chatController.room,
)
: ITFeedbackCard(
req: ITFeedbackRequestModel(
sourceText: sourceText!,
currentText: controller.choreographer.currentText,
chosenContinuance:
controller.currentITStep!.continuances[index].text,
bestContinuance: controller.currentITStep!.best.text,
feedbackLang: controller.targetLangCode,
sourceTextLang: controller.sourceLangCode,
targetLang: controller.targetLangCode,
),
cardSize: const Size(300, 300),
borderColor: borderColor,
transformTargetId: controller.choreographer.itBarTransformTargetKey,
backDropToDismiss: false,
);
choiceFeedback: choiceFeedback,
),
cardSize: const Size(300, 300),
borderColor: borderColor,
transformTargetId: controller.choreographer.itBarTransformTargetKey,
backDropToDismiss: false,
);
}
@override
Widget build(BuildContext context) {

@ -1,7 +1,6 @@
import 'dart:developer';
import 'dart:math';
import 'package:fluffychat/pangea/constants/class_default_values.dart';
import 'package:fluffychat/pangea/controllers/class_controller.dart';
import 'package:fluffychat/pangea/controllers/contextual_definition_controller.dart';
import 'package:fluffychat/pangea/controllers/language_controller.dart';
@ -225,19 +224,19 @@ class PangeaController {
continue;
}
final List<String> userIds = participants.map((user) => user.id).toList();
if (space.canInvite && !userIds.contains(BotName.byEnvironment)) {
try {
await space.invite(BotName.byEnvironment);
await space.setPower(
BotName.byEnvironment,
ClassDefaultValues.powerLevelOfAdmin,
);
} catch (err) {
ErrorHandler.logError(
e: "Failed to invite pangea bot to space ${space.id}",
);
}
}
// if (space.canInvite && !userIds.contains(BotName.byEnvironment)) {
// try {
// await space.invite(BotName.byEnvironment);
// await space.setPower(
// BotName.byEnvironment,
// ClassDefaultValues.powerLevelOfAdmin,
// );
// } catch (err) {
// ErrorHandler.logError(
// e: "Failed to invite pangea bot to space ${space.id}",
// );
// }
// }
}
}
}

@ -6,6 +6,7 @@ import 'package:fluffychat/pangea/config/environment.dart';
import 'package:fluffychat/pangea/constants/model_keys.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/network/urls.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import '../network/requests.dart';

@ -562,7 +562,6 @@ extension PangeaRoom on Room {
event: event,
timeline: timeline,
ownMessage: true,
selected: false,
);
msgs.add(
RecentMessageRecord(

@ -283,10 +283,20 @@ class IGCTextData {
nextTokenIndex = matchTokens.length;
}
final String matchText = originalInput.substring(
matchTokens[tokenIndex].token.text.offset,
matchTokens[nextTokenIndex - 1].token.end,
);
String matchText;
try {
matchText = originalInput.substring(
matchTokens[tokenIndex].token.text.offset,
matchTokens[nextTokenIndex - 1].token.end,
);
} catch (err) {
return [
TextSpan(
text: originalInput,
style: defaultStyle,
),
];
}
items.add(
TextSpan(

@ -8,6 +8,7 @@ import 'package:fluffychat/pangea/constants/pangea_message_types.dart';
import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/models/choreo_record.dart';
import 'package:fluffychat/pangea/models/class_model.dart';
import 'package:fluffychat/pangea/models/message_data_models.dart';
import 'package:fluffychat/pangea/models/pangea_representation_event.dart';
import 'package:fluffychat/pangea/utils/bot_name.dart';
@ -25,14 +26,13 @@ class PangeaMessageEvent {
late Event _event;
final Timeline timeline;
final bool ownMessage;
final bool selected;
bool _isValidPangeaMessageEvent = true;
RepresentationEvent? _displayRepresentation;
PangeaMessageEvent({
required Event event,
required this.timeline,
required this.ownMessage,
required this.selected,
}) {
if (event.type != EventTypes.Message) {
_isValidPangeaMessageEvent = false;
@ -46,6 +46,8 @@ class PangeaMessageEvent {
//the timeline filters the edits and uses the original events
//so this event will always be the original and the sdk getter body
//handles getting the latest text from the aggregated events
Event get event => _event;
String get body => _event.body;
String get senderId => _event.senderId;
@ -79,7 +81,7 @@ class PangeaMessageEvent {
if ([EventStatus.error, EventStatus.sending].contains(_event.status)) {
return false;
}
if (ownMessage && !selected) return false;
// if (ownMessage && !selected) return false;
return true;
}
@ -87,13 +89,13 @@ class PangeaMessageEvent {
//get audio for text and language
//if no audio exists, create it
//if audio exists, return it
Future<String?> getAudioGlobal(String langCode) async {
Future<Event?> getAudioGlobal(String langCode) async {
// try {
final String text = representationByLanguage(langCode)?.text ?? body;
final local = getAudioLocal(langCode, text);
if (local != null) return Future.value(local.eventId);
if (local != null) return Future.value(local);
final TextToSpeechRequest params = TextToSpeechRequest(
text: text,
@ -132,69 +134,61 @@ class PangeaMessageEvent {
throw Exception("Unexpected mime type: ${file.mimeType}");
}
return room.sendFileEvent(
file,
inReplyTo: _event,
extraContent: {
'info': {
...file.info,
'duration': response.durationMillis,
},
'org.matrix.msc3245.voice': {},
'org.matrix.msc1767.audio': {
'duration': response.durationMillis,
'waveform': response.waveform,
try {
final String? eventId = await room.sendFileEvent(
file,
inReplyTo: _event,
extraContent: {
'info': {
...file.info,
'duration': response.durationMillis,
},
'org.matrix.msc3245.voice': {},
'org.matrix.msc1767.audio': {
'duration': response.durationMillis,
'waveform': response.waveform,
},
ModelKey.transcription: {
ModelKey.text: text,
ModelKey.langCode: langCode,
},
},
ModelKey.transcription: {
ModelKey.text: text,
ModelKey.langCode: langCode,
},
},
).timeout(
Durations.long4,
onTimeout: () {
debugPrint("timeout in getAudioGlobal");
return null;
},
).then((eventId) {
);
// .timeout(
// Durations.long4,
// onTimeout: () {
// debugPrint("timeout in getAudioGlobal");
// return null;
// },
// );
debugPrint("eventId in getAudioGlobal $eventId");
return eventId;
}).catchError((err, s) {
return eventId != null ? room.getEventById(eventId) : null;
} catch (err) {
debugPrint("error in getAudioGlobal");
debugPrint(err);
debugPrint(s);
debugger(when: kDebugMode);
return null;
});
// } catch (err, s) {
// debugger(when: kDebugMode);
// ErrorHandler.logError(
// e: err,
// s: s,
// );
// return Future.value(null);
// }
}
}
Event? getAudioLocal(String langCode, String text) {
return allAudio.firstWhereOrNull(
(element) {
// Safely access the transcription map
final transcription = element.content.tryGet(ModelKey.transcription);
final transcription = element.content.tryGetMap(ModelKey.transcription);
return transcription != null;
// if (transcription == null) {
// // If transcription is null, this element does not match.
// return false;
// }
// return transcription != null;
if (transcription == null) {
// If transcription is null, this element does not match.
return false;
}
// // Safely get language code and text from the transcription
// final elementLangCode = transcription.tryGet(ModelKey.langCode);
// final elementText = transcription.tryGet(ModelKey.text);
// Safely get language code and text from the transcription
final elementLangCode = transcription[ModelKey.langCode];
final elementText = transcription[ModelKey.text];
// // Check if both language code and text match
// return elementLangCode == langCode && elementText == text;
// Check if both language code and text match
return elementLangCode == langCode && elementText == text;
},
);
}
@ -397,14 +391,53 @@ class PangeaMessageEvent {
!room.isUserSpaceAdmin(_event.senderId) &&
_event.messageType != PangeaMessageTypes.report;
String get messageDisplayLangCode {
final bool immersionMode = MatrixState
.pangeaController.permissionsController
.isToolEnabled(ToolSetting.immersionMode, room);
final String? l2Code = MatrixState.pangeaController.languageController
.activeL2Code(roomID: room.id);
final String? langCode = immersionMode ? l2Code : originalWritten?.langCode;
return langCode ?? LanguageKeys.unknownLanguage;
}
Future<RepresentationEvent?> getDisplayRepresentation(
BuildContext context,
) async {
if (messageDisplayLangCode == LanguageKeys.unknownLanguage) return null;
if (_displayRepresentation != null) return _displayRepresentation;
_displayRepresentation = representationByLanguage(messageDisplayLangCode);
if (_displayRepresentation != null) {
return _displayRepresentation;
}
try {
_displayRepresentation = await representationByLanguageGlobal(
context: context,
langCode: messageDisplayLangCode,
);
return _displayRepresentation;
} catch (err, s) {
ErrorHandler.logError(
m: "error in getDisplayRepresentation",
e: err,
s: s,
);
return null;
}
}
String get displayMessageText => _displayRepresentation?.text ?? body;
// List<SpanData> get activities =>
//each match is turned into an activity that other students can access
//they're not told the answer but have to find it themselves
//the message has a blank piece which they fill in themselves
// replication of logic from message_content.dart
bool get isHtml =>
AppConfig.renderHtml && !_event.redacted && _event.isRichMessage;
// bool get isHtml =>
// AppConfig.renderHtml && !_event.redacted && _event.isRichMessage;
}
class URLFinder {

@ -10,7 +10,6 @@ import 'package:fluffychat/pangea/config/environment.dart';
/// https://api.staging.pangea.chat/api/v1/
class PApiUrls {
static String baseAPI = Environment.baseAPI;
static String choreoBaseApi = Environment.choreoApi;
/// ---------------------- Languages --------------------------------------
static String getLanguages = "/languages";
@ -51,7 +50,7 @@ class PApiUrls {
static String firstStep = "/it_initialstep";
static String subseqStep = "/it_step";
static String textToSpeech = "$choreoBaseApi/text_to_speech";
static String textToSpeech = "${Environment.choreoApi}/text_to_speech";
///-------------------------------- revenue cat --------------------------
static String rcApiV1 = "https://api.revenuecat.com/v1";

@ -14,7 +14,7 @@ class ITRepo {
CustomInputRequestModel initalText,
) async {
final Requests req = Requests(
baseUrl: PApiUrls.choreoBaseApi,
baseUrl: Environment.choreoApi,
choreoApiKey: Environment.choreoApiKey,
);
final Response res =
@ -29,7 +29,7 @@ class ITRepo {
SystemChoiceRequestModel subseqText,
) async {
final Requests req = Requests(
baseUrl: PApiUrls.choreoBaseApi,
baseUrl: Environment.choreoApi,
choreoApiKey: Environment.choreoApiKey,
);

@ -8,7 +8,7 @@ class MessageServiceRepo {
String messageId,
) async {
final Requests req = Requests(
baseUrl: PApiUrls.choreoBaseApi,
baseUrl: Environment.choreoApi,
choreoApiKey: Environment.choreoApiKey,
);

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:sentry_flutter/sentry_flutter.dart';

@ -136,7 +136,6 @@ List<PangeaMessageEvent> getPangeaMessageEvents(
event: message,
timeline: timeline,
ownMessage: false,
selected: false,
),
)
.cast<PangeaMessageEvent>()

@ -17,9 +17,11 @@ class GetChatListItemSubtitle {
) async {
if (event == null) return L10n.of(context)!.emptyChat;
// try {
if (event.type != EventTypes.Message ||
!pangeaController.permissionsController
.isToolEnabled(ToolSetting.immersionMode, event.room)) {
if (event.type != EventTypes.Message)
// ||
// !pangeaController.permissionsController
// .isToolEnabled(ToolSetting.immersionMode, event.room))
{
return event.calcLocalizedBody(
MatrixLocals(L10n.of(context)!),
hideReply: true,
@ -31,13 +33,17 @@ class GetChatListItemSubtitle {
);
}
String? eventContextId = event.eventId;
if (!event.eventId.isValidMatrixId || event.eventId.sigil != '\$') {
eventContextId = null;
}
final Timeline timeline =
await event.room.getTimeline(eventContextId: event.eventId);
await event.room.getTimeline(eventContextId: eventContextId);
final PangeaMessageEvent pangeaMessageEvent = PangeaMessageEvent(
event: event,
timeline: timeline,
ownMessage: false,
selected: false,
);
final l2Code =
pangeaController.languageController.activeL2Code(roomID: event.roomId);

@ -15,11 +15,16 @@ class OverlayUtil {
static showOverlay({
required BuildContext context,
required Widget child,
required Size size,
required String transformTargetId,
// Size? size,
double? width,
double? height,
Offset? offset,
backDropToDismiss = true,
Color? borderColor,
Color? backgroundColor,
Alignment? targetAnchor,
Alignment? followerAnchor,
}) {
try {
MatrixState.pAnyState.closeOverlay();
@ -27,35 +32,37 @@ class OverlayUtil {
MatrixState.pAnyState.layerLinkAndKey(transformTargetId);
final OverlayEntry entry = OverlayEntry(
builder: (context) => Stack(
children: [
// GestureDetector to detect when dismissed by clicking outside
Positioned.fill(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
MatrixState.pAnyState.closeOverlay();
},
builder: (context) => AnimatedContainer(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
child: Stack(
children: [
if (backDropToDismiss)
TransparentBackdrop(
backgroundColor: backgroundColor,
),
Positioned(
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,
),
),
),
if (backDropToDismiss) const TransparentBackdrop(),
Positioned(
width: size.width,
height: size.height,
child: CompositedTransformFollower(
link: layerLinkAndKey.link,
showWhenUnlinked: false,
offset: offset ?? Offset.zero,
child: child,
),
),
],
],
),
),
);
MatrixState.pAnyState.openOverlay(entry, context);
} catch (err, stack) {
debugger(when: kDebugMode);
debugPrint("ERROR: $err");
debugPrint("STACK: $stack");
// debugger(when: kDebugMode);
ErrorHandler.logError(e: err, s: stack);
}
}
@ -90,7 +97,8 @@ class OverlayUtil {
showOverlay(
context: context,
child: child,
size: cardSize,
width: cardSize.width,
height: cardSize.height,
transformTargetId: transformTargetId,
offset: cardOffset,
backDropToDismiss: backDropToDismiss,
@ -174,15 +182,17 @@ class OverlayUtil {
}
class TransparentBackdrop extends StatelessWidget {
final Color? backgroundColor;
const TransparentBackdrop({
super.key,
this.backgroundColor,
});
@override
Widget build(BuildContext context) {
return Material(
borderOnForeground: false,
color: Colors.transparent,
color: backgroundColor ?? Colors.transparent,
clipBehavior: Clip.antiAlias,
child: InkWell(
hoverColor: Colors.transparent,

@ -1,159 +0,0 @@
import 'dart:async';
import 'package:fluffychat/pangea/utils/any_state_holder.dart';
import 'package:fluffychat/pangea/utils/overlay.dart';
import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
class ShowDefintionUtil {
String messageText;
final String langCode;
final String targetId;
final FocusNode focusNode = FocusNode();
final Room room;
String? textSelection;
bool inCooldown = false;
double? dx;
double? dy;
ShowDefintionUtil({
required this.targetId,
required this.room,
required this.langCode,
required this.messageText,
});
void onTextSelection({
required BuildContext context,
TextSelection? selectedText,
SelectedContent? selectedContent,
SelectionChangedCause? cause,
}) {
if ((selectedText == null && selectedContent == null) ||
selectedText?.isCollapsed == true) {
clearTextSelection();
return;
}
textSelection = selectedText != null
? selectedText.textInside(messageText)
: selectedContent!.plainText;
if (BrowserContextMenu.enabled && kIsWeb) {
BrowserContextMenu.disableContextMenu();
}
if (kIsWeb && cause != SelectionChangedCause.tap) {
handleToolbar(context);
}
}
void clearTextSelection() {
textSelection = null;
if (kIsWeb && !BrowserContextMenu.enabled) {
BrowserContextMenu.enableContextMenu();
}
}
void handleToolbar(BuildContext context) async {
if (inCooldown || OverlayUtil.isOverlayOpen || !kIsWeb) return;
inCooldown = true;
Timer(const Duration(milliseconds: 750), () => inCooldown = false);
await Future.delayed(const Duration(milliseconds: 750));
showToolbar(context);
}
void showDefinition(BuildContext context) {
if (textSelection == null) return;
OverlayUtil.showPositionedCard(
context: context,
cardToShow: WordDataCard(
word: textSelection!,
wordLang: langCode,
fullText: messageText,
fullTextLang: langCode,
hasInfo: false,
room: room,
),
cardSize: const Size(300, 300),
transformTargetId: targetId,
backDropToDismiss: false,
);
}
// web toolbar
Future<dynamic> showToolbar(BuildContext context) async {
final LayerLinkAndKey layerLinkAndKey =
MatrixState.pAnyState.layerLinkAndKey(targetId);
final RenderObject? targetRenderBox =
layerLinkAndKey.key.currentContext!.findRenderObject();
final Offset transformTargetOffset =
(targetRenderBox as RenderBox).localToGlobal(Offset.zero);
if (dx != null && dx! > MediaQuery.of(context).size.width - 130) {
dx = MediaQuery.of(context).size.width - 130;
}
final double xOffset = dx != null ? dx! - transformTargetOffset.dx : 0;
final double yOffset =
dy != null ? dy! - transformTargetOffset.dy + 10 : 10;
OverlayUtil.showOverlay(
context: context,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: Size.zero,
padding: EdgeInsets.zero,
),
onPressed: () {
showDefinition(context);
},
child: Text(
L10n.of(context)!.showDefinition,
style: const TextStyle(
fontSize: 14,
),
),
),
size: const Size(130, 45),
transformTargetId: targetId,
offset: Offset(xOffset, yOffset),
);
}
void onMouseRegionUpdate(PointerEvent event) {
dx = event.position.dx;
dy = event.position.dy;
}
Widget contextMenuOverride({
required BuildContext context,
EditableTextState? textSelection,
SelectableRegionState? contentSelection,
}) {
if (textSelection == null && contentSelection == null) {
return const SizedBox();
}
return AdaptiveTextSelectionToolbar.buttonItems(
anchors: textSelection?.contextMenuAnchors ??
contentSelection!.contextMenuAnchors,
buttonItems: [
if (textSelection != null) ...textSelection.contextMenuButtonItems,
if (contentSelection != null)
...contentSelection.contextMenuButtonItems,
ContextMenuButtonItem(
label: L10n.of(context)!.showDefinition,
onPressed: () {
showDefinition(context);
focusNode.unfocus();
},
),
],
);
}
}

@ -1,303 +0,0 @@
import 'dart:async';
import 'package:fluffychat/pangea/utils/any_state_holder.dart';
import 'package:fluffychat/pangea/utils/overlay.dart';
import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
enum MessageMode { translation, play, definition, image, spellCheck }
class MessageOverlayController {
OverlayEntry? _overlayEntry;
final BuildContext _context;
final GlobalKey _targetKey;
MessageMode? _currentMode;
AnimationController? _animationController;
MessageOverlayController(this._context, this._targetKey) {
_animationController = AnimationController(
vsync: Navigator.of(_context), // Using the Navigator's TickerProvider
duration: const Duration(milliseconds: 300),
);
}
void showOverlay() {
final RenderBox renderBox =
_targetKey.currentContext?.findRenderObject() as RenderBox;
final Offset offset = renderBox.localToGlobal(Offset.zero);
final Size size = renderBox.size;
final double screenWidth = MediaQuery.of(_context).size.width;
// Determines if there is more room above or below the RenderBox
final bool isBottomRoomAvailable =
MediaQuery.of(_context).size.height - (offset.dy + size.height) >=
size.height;
final double topPosition = isBottomRoomAvailable
? offset.dy + size.height
: offset.dy - size.height;
// Ensure the overlay does not overflow the screen horizontally
double leftPosition = offset.dx + size.width / 2 - screenWidth / 2;
leftPosition = leftPosition < 0 ? 0 : leftPosition;
final double rightPosition =
leftPosition + screenWidth > MediaQuery.of(_context).size.width
? MediaQuery.of(_context).size.width - leftPosition - screenWidth
: leftPosition;
_overlayEntry = OverlayEntry(
builder: (context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return AnimatedPositioned(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
left: leftPosition,
right: rightPosition,
top: isBottomRoomAvailable ? topPosition : null,
bottom: isBottomRoomAvailable
? null
: MediaQuery.of(_context).size.height -
topPosition -
size.height,
child: AnimatedSize(
curve: Curves.easeInOut,
duration: const Duration(milliseconds: 300),
child: Material(
elevation: 4.0,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Row(
mainAxisSize: MainAxisSize.min,
children: MessageMode.values.map((mode) {
return IconButton(
icon: Icon(_getIconData(mode)),
onPressed: () {
setState(() {
_currentMode = mode;
});
_animationController?.forward();
},
);
}).toList(),
),
SizeTransition(
sizeFactor: CurvedAnimation(
parent: _animationController!,
curve: Curves.fastOutSlowIn,
),
axisAlignment: -1.0,
child: _buildModeContent(),
),
],
),
),
),
);
},
);
},
);
Overlay.of(_context).insert(_overlayEntry!);
}
void hideOverlay() {
_overlayEntry?.remove();
_overlayEntry = null;
_animationController?.reverse();
}
Widget _buildModeContent() {
switch (_currentMode) {
case MessageMode.translation:
return const Text('Translation Mode');
case MessageMode.play:
return const Text('Play Mode');
case MessageMode.definition:
return const Text('Definition Mode');
case MessageMode.image:
return const Text('Image Mode');
case MessageMode.spellCheck:
return const Text('SpellCheck Mode');
default:
return const SizedBox
.shrink(); // Empty container for the default case, meaning no content
}
}
IconData _getIconData(MessageMode mode) {
switch (mode) {
case MessageMode.translation:
return Icons.g_translate;
case MessageMode.play:
return Icons.play_arrow;
case MessageMode.definition:
return Icons.book;
case MessageMode.image:
return Icons.image;
case MessageMode.spellCheck:
return Icons.spellcheck;
default:
return Icons.error; // Icon to indicate an error or unsupported mode
}
}
void dispose() {
_overlayEntry?.dispose();
_animationController?.dispose();
}
}
class ShowDefintionUtil {
String messageText;
final String langCode;
final String targetId;
final FocusNode focusNode = FocusNode();
final Room room;
String? textSelection;
bool inCooldown = false;
double? dx;
double? dy;
ShowDefintionUtil({
required this.targetId,
required this.room,
required this.langCode,
required this.messageText,
});
void onTextSelection({
required BuildContext context,
TextSelection? selectedText,
SelectedContent? selectedContent,
SelectionChangedCause? cause,
}) {
if ((selectedText == null && selectedContent == null) ||
selectedText?.isCollapsed == true) {
clearTextSelection();
return;
}
textSelection = selectedText != null
? selectedText.textInside(messageText)
: selectedContent!.plainText;
if (BrowserContextMenu.enabled && kIsWeb) {
BrowserContextMenu.disableContextMenu();
}
if (kIsWeb && cause != SelectionChangedCause.tap) {
handleToolbar(context);
}
}
void clearTextSelection() {
textSelection = null;
if (kIsWeb && !BrowserContextMenu.enabled) {
BrowserContextMenu.enableContextMenu();
}
}
void handleToolbar(BuildContext context) async {
if (inCooldown || OverlayUtil.isOverlayOpen || !kIsWeb) return;
inCooldown = true;
Timer(const Duration(milliseconds: 750), () => inCooldown = false);
await Future.delayed(const Duration(milliseconds: 750));
showToolbar(context);
}
void showDefinition(BuildContext context) {
if (textSelection == null) return;
OverlayUtil.showPositionedCard(
context: context,
cardToShow: WordDataCard(
word: textSelection!,
wordLang: langCode,
fullText: messageText,
fullTextLang: langCode,
hasInfo: false,
room: room,
),
cardSize: const Size(300, 300),
transformTargetId: targetId,
backDropToDismiss: false,
);
}
// web toolbar
Future<dynamic> showToolbar(BuildContext context) async {
final LayerLinkAndKey layerLinkAndKey =
MatrixState.pAnyState.layerLinkAndKey(targetId);
final RenderObject? targetRenderBox =
layerLinkAndKey.key.currentContext!.findRenderObject();
final Offset transformTargetOffset =
(targetRenderBox as RenderBox).localToGlobal(Offset.zero);
if (dx != null && dx! > MediaQuery.of(context).size.width - 130) {
dx = MediaQuery.of(context).size.width - 130;
}
final double xOffset = dx != null ? dx! - transformTargetOffset.dx : 0;
final double yOffset =
dy != null ? dy! - transformTargetOffset.dy + 10 : 10;
OverlayUtil.showOverlay(
context: context,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: Size.zero,
padding: EdgeInsets.zero,
),
onPressed: () {
showDefinition(context);
},
child: Text(
L10n.of(context)!.showDefinition,
style: const TextStyle(
fontSize: 14,
),
),
),
size: const Size(130, 45),
transformTargetId: targetId,
offset: Offset(xOffset, yOffset),
);
}
void onMouseRegionUpdate(PointerEvent event) {
dx = event.position.dx;
dy = event.position.dy;
}
Widget contextMenuOverride({
required BuildContext context,
EditableTextState? textSelection,
SelectableRegionState? contentSelection,
}) {
if (textSelection == null && contentSelection == null) {
return const SizedBox();
}
return AdaptiveTextSelectionToolbar.buttonItems(
anchors: textSelection?.contextMenuAnchors ??
contentSelection!.contextMenuAnchors,
buttonItems: [
if (textSelection != null) ...textSelection.contextMenuButtonItems,
if (contentSelection != null)
...contentSelection.contextMenuButtonItems,
ContextMenuButtonItem(
label: L10n.of(context)!.showDefinition,
onPressed: () {
showDefinition(context);
focusNode.unfocus();
},
),
],
);
}
}

@ -0,0 +1,149 @@
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat/events/audio_player.dart';
import 'package:fluffychat/pangea/models/pangea_message_event.dart';
import 'package:fluffychat/pangea/models/pangea_representation_event.dart';
import 'package:fluffychat/pangea/utils/bot_style.dart';
import 'package:fluffychat/pangea/utils/error_handler.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 MessageAudioCard extends StatefulWidget {
final PangeaMessageEvent messageEvent;
const MessageAudioCard({
super.key,
required this.messageEvent,
});
@override
MessageAudioCardState createState() => MessageAudioCardState();
}
class MessageAudioCardState extends State<MessageAudioCard> {
// RepresentationEvent? repEvent;
bool _isLoading = false;
Event? localAudioEvent;
// String langCode = "en";
// void setLangCode() {
// final String? l2Code =
// MatrixState.pangeaController.languageController.activeL2Code(
// roomID: widget.messageEvent.room.id,
// );
// setState(() => langCode = l2Code ?? "en");
// }
// void fetchRepresentation(BuildContext context) {
// repEvent = widget.messageEvent.representationByLanguage(
// langCode,
// );
// if (repEvent == null) {
// setState(() => _isLoading = true);
// widget.messageEvent
// .representationByLanguageGlobal(
// context: context,
// langCode: langCode,
// )
// .onError((error, stackTrace) => ErrorHandler.logError())
// .then(((RepresentationEvent? event) => repEvent = event))
// .whenComplete(
// () => setState(() => _isLoading = false),
// );
// }
// }
void fetchAudio() {
if (!mounted) return;
// final String? text = widget.messageEvent.displayMessageText;
// if (text == null || text.isEmpty) return;
setState(() => _isLoading = true);
widget.messageEvent
.getAudioGlobal(widget.messageEvent.messageDisplayLangCode)
.then((Event? event) {
localAudioEvent = event;
}).catchError((e) {
if (!mounted) return null;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.errorGettingAudio),
),
);
return null;
}).whenComplete(() {
if (mounted) setState(() => _isLoading = false);
});
}
@override
void initState() {
super.initState();
widget.messageEvent
.getDisplayRepresentation(context)
.then((_) => fetchAudio());
}
@override
Widget build(BuildContext context) {
final playButton = InkWell(
borderRadius: BorderRadius.circular(64),
onTap: () => widget.messageEvent
.getDisplayRepresentation(context)
.then((event) => event == null ? null : fetchAudio),
child: Material(
color: AppConfig.primaryColor.withAlpha(64),
borderRadius: BorderRadius.circular(64),
child: const Icon(
// Change the icon based on some condition. If you have an audio player state, use it here.
Icons.play_arrow_outlined,
color: AppConfig.primaryColor,
),
),
);
return Padding(
padding: const EdgeInsets.all(8),
child: _isLoading
? SizedBox(
height: 14,
width: 14,
child: CircularProgressIndicator(
strokeWidth: 2.0,
color: Theme.of(context).colorScheme.primary,
),
)
: localAudioEvent != null
? Container(
constraints: const BoxConstraints(
maxWidth: 250,
),
child: Column(
children: [
AudioPlayerWidget(
localAudioEvent!,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
],
),
)
:
// Opacity(
// opacity: widget.messageEvent.getDisplayRepresentation().then((event) => event == null ? ) == null
// ? 0.5
// : 1,
// // child: SizedBox(
// // width: 44,
// // height: 36,
// child:
Padding(
padding: const EdgeInsets.only(left: 8),
child: playButton,
),
// ),
// ),
);
}
}

@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
class MessageContextMenu {
static List<ContextMenuButtonItem> customToolbarOptions(
BuildContext context,
void Function()? onDefine,
void Function()? onListen,
) {
return [
ContextMenuButtonItem(
label: L10n.of(context)!.define,
onPressed: onDefine,
),
ContextMenuButtonItem(
label: L10n.of(context)!.listen,
onPressed: onListen,
),
];
}
static List<ContextMenuButtonItem> toolbarOptions(
EditableTextState? textSelection,
SelectableRegionState? contentSelection,
BuildContext context,
void Function()? onDefine,
void Function()? onListen,
) {
final List<ContextMenuButtonItem> menuItems =
textSelection?.contextMenuButtonItems ??
contentSelection?.contextMenuButtonItems ??
[];
menuItems.sort((a, b) {
if (a.type == ContextMenuButtonType.copy) return -1;
if (b.type == ContextMenuButtonType.copy) return 1;
return 0;
});
return MessageContextMenu.customToolbarOptions(
context, onDefine, onListen) +
menuItems;
}
static Widget contextMenuOverride({
required BuildContext context,
EditableTextState? textSelection,
SelectableRegionState? contentSelection,
void Function()? onDefine,
void Function()? onListen,
}) {
if (textSelection == null && contentSelection == null) {
return const SizedBox();
}
final List<ContextMenuButtonItem> menuItems =
MessageContextMenu.toolbarOptions(
textSelection,
contentSelection,
context,
onDefine,
onListen,
);
return AdaptiveTextSelectionToolbar.buttonItems(
anchors: textSelection?.contextMenuAnchors ??
contentSelection!.contextMenuAnchors,
buttonItems: menuItems,
);
}
}

@ -0,0 +1,34 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class MessageTextSelection {
String? selectedText;
String messageText = "";
void setMessageText(String text) {
messageText = text;
}
void onTextSelection(TextSelection selection) => selection.isCollapsed == true
? clearTextSelection()
: setTextSelection(selection);
void setTextSelection(TextSelection selection) {
selectedText = selection.textInside(messageText);
if (BrowserContextMenu.enabled && kIsWeb) {
BrowserContextMenu.disableContextMenu();
}
// selectionStream.add(selectedText);
}
void clearTextSelection() {
selectedText = null;
if (kIsWeb && !BrowserContextMenu.enabled) {
BrowserContextMenu.enableContextMenu();
}
// selectionStream.add(selectedText);
}
}

@ -0,0 +1,303 @@
import 'dart:async';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/models/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_text_selection.dart';
import 'package:fluffychat/pangea/widgets/chat/message_translation_card.dart';
import 'package:fluffychat/pangea/widgets/chat/overlay_message.dart';
import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
enum MessageMode { translation, play, definition }
class ToolbarDisplayController {
final FocusNode focusNode = FocusNode();
final PangeaMessageEvent pangeaMessageEvent;
final String targetId;
final bool immersionMode;
final ChatController controller;
MessageToolbar? toolbar;
String? overlayId;
double? messageWidth;
final toolbarModeStream = StreamController<MessageMode>.broadcast();
ToolbarDisplayController({
required this.pangeaMessageEvent,
required this.targetId,
required this.immersionMode,
required this.controller,
});
void setToolbar() {
toolbar ??= MessageToolbar(
textSelection: MessageTextSelection(),
room: pangeaMessageEvent.room,
toolbarModeStream: toolbarModeStream,
pangeaMessageEvent: pangeaMessageEvent,
immersionMode: immersionMode,
controller: controller,
);
final LayerLinkAndKey layerLinkAndKey =
MatrixState.pAnyState.layerLinkAndKey(targetId);
final targetRenderBox =
layerLinkAndKey.key.currentContext?.findRenderObject();
if (targetRenderBox == null) return;
final Size transformTargetSize = (targetRenderBox as RenderBox).size;
messageWidth = transformTargetSize.width;
}
void showToolbar(BuildContext context, {MessageMode? mode}) {
if (highlighted) return;
if (controller.selectMode) {
controller.clearSelectedEvents();
}
focusNode.unfocus();
Widget overlayEntry;
try {
overlayEntry = Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: pangeaMessageEvent.ownMessage
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [
toolbar!,
OverlayMessage(
pangeaMessageEvent.event,
timeline: pangeaMessageEvent.timeline,
immersionMode: immersionMode,
ownMessage: pangeaMessageEvent.ownMessage,
toolbarController: this,
width: messageWidth,
),
],
);
} catch (err) {
ErrorHandler.logError(e: err, s: StackTrace.current);
return;
}
OverlayUtil.showOverlay(
context: context,
child: overlayEntry,
transformTargetId: targetId,
targetAnchor: pangeaMessageEvent.ownMessage
? Alignment.bottomRight
: Alignment.bottomLeft,
followerAnchor: pangeaMessageEvent.ownMessage
? Alignment.bottomRight
: Alignment.bottomLeft,
backgroundColor: const Color.fromRGBO(0, 0, 0, 1).withAlpha(164),
);
if (MatrixState.pAnyState.overlay != null) {
overlayId = MatrixState.pAnyState.overlay.hashCode.toString();
}
if (mode != null) {
Future.delayed(
const Duration(milliseconds: 100),
() => toolbarModeStream.add(mode),
);
}
}
bool get highlighted =>
MatrixState.pAnyState.overlay.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;
const MessageToolbar({
super.key,
required this.textSelection,
required this.room,
required this.pangeaMessageEvent,
required this.toolbarModeStream,
required this.immersionMode,
required this.controller,
});
@override
MessageToolbarState createState() => MessageToolbarState();
}
class MessageToolbarState extends State<MessageToolbar> {
Widget? child;
MessageMode? _currentMode;
late StreamSubscription<String?> _selectionStream;
late StreamSubscription<MessageMode> _toolbarModeStream;
IconData _getIconData(MessageMode mode) {
switch (mode) {
case MessageMode.translation:
return Icons.g_translate;
case MessageMode.play:
return Icons.play_arrow;
case MessageMode.definition:
return Icons.book;
default:
return Icons.error; // Icon to indicate an error or unsupported mode
}
}
bool _enabledButton(MessageMode mode) {
switch (mode) {
case MessageMode.translation:
return true;
case MessageMode.play:
return true;
case MessageMode.definition:
debugPrint("checking");
return widget.textSelection.selectedText != null;
default:
return false;
}
}
void updateMode(MessageMode newMode) {
debugPrint("updating toolbar mode");
setState(() => _currentMode = newMode);
switch (_currentMode) {
case MessageMode.translation:
showTranslation();
break;
case MessageMode.play:
playAudio();
break;
case MessageMode.definition:
showDefinition();
break;
default:
break;
}
setState(() {});
}
void showTranslation() {
debugPrint("show translation");
child = MessageTranslationCard(
messageEvent: widget.pangeaMessageEvent,
immersionMode: widget.immersionMode,
);
}
void playAudio() {
debugPrint("play audio");
child = MessageAudioCard(
messageEvent: widget.pangeaMessageEvent,
);
}
void showDefinition() {
if (widget.textSelection.selectedText == null ||
widget.textSelection.selectedText!.isEmpty) {
return;
}
child = WordDataCard(
word: widget.textSelection.selectedText!,
wordLang: widget.pangeaMessageEvent.messageDisplayLangCode,
fullText: widget.textSelection.messageText,
fullTextLang: widget.pangeaMessageEvent.messageDisplayLangCode,
hasInfo: false,
room: widget.room,
);
}
void showImage() {}
void spellCheck() {}
void showMore() {
MatrixState.pAnyState.closeOverlay();
widget.controller.onSelectMessage(widget.pangeaMessageEvent.event);
}
@override
void initState() {
super.initState();
_toolbarModeStream = widget.toolbarModeStream.stream.listen((mode) {
updateMode(mode);
});
}
@override
void dispose() {
_selectionStream.cancel();
_toolbarModeStream.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
border: Border.all(
width: 2,
color: Theme.of(context).colorScheme.primary,
),
borderRadius: const BorderRadius.all(
Radius.circular(25),
),
),
constraints: const BoxConstraints(
maxWidth: 300,
minWidth: 300,
maxHeight: 300,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: SingleChildScrollView(
child: AnimatedSize(
duration: FluffyThemes.animationDuration,
child: Column(
children: [
child ?? const SizedBox(),
SizedBox(height: child == null ? 0 : 20),
],
),
),
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: MessageMode.values.map((mode) {
return IconButton(
icon: Icon(_getIconData(mode)),
onPressed:
_enabledButton(mode) ? () => updateMode(mode) : null,
);
}).toList() +
[
IconButton(
icon: Icon(Icons.adaptive.more_outlined),
onPressed: showMore,
),
],
),
],
),
),
);
}
}

@ -0,0 +1,103 @@
import 'package:fluffychat/pangea/models/pangea_message_event.dart';
import 'package:fluffychat/pangea/models/pangea_representation_event.dart';
import 'package:fluffychat/pangea/utils/bot_style.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
class MessageTranslationCard extends StatefulWidget {
final PangeaMessageEvent messageEvent;
final bool immersionMode;
const MessageTranslationCard({
super.key,
required this.messageEvent,
required this.immersionMode,
});
@override
MessageTranslationCardState createState() => MessageTranslationCardState();
}
class MessageTranslationCardState extends State<MessageTranslationCard> {
RepresentationEvent? repEvent;
bool _fetchingRepresentation = false;
String? translationLangCode() {
final String? l1Code =
MatrixState.pangeaController.languageController.activeL1Code(
roomID: widget.messageEvent.room.id,
);
if (widget.immersionMode) return l1Code;
final String? l2Code =
MatrixState.pangeaController.languageController.activeL2Code(
roomID: widget.messageEvent.room.id,
);
final String? originalWrittenCode =
widget.messageEvent.originalWritten?.content.langCode;
return l1Code == originalWrittenCode ? l2Code : l1Code;
}
void fetchRepresentation(BuildContext context) {
final String? langCode = translationLangCode();
if (langCode == null) return;
repEvent = widget.messageEvent.representationByLanguage(
langCode,
);
if (repEvent == null && mounted) {
setState(() => _fetchingRepresentation = true);
widget.messageEvent
.representationByLanguageGlobal(
context: context,
langCode: langCode,
)
.onError(
(error, stackTrace) => ErrorHandler.logError(
e: error,
s: stackTrace,
),
)
.then((RepresentationEvent? event) => repEvent = event)
.whenComplete(
() => setState(() => _fetchingRepresentation = false),
);
} else {
setState(() {});
}
}
@override
void initState() {
super.initState();
fetchRepresentation(context);
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8),
child: _fetchingRepresentation
? SizedBox(
height: 14,
width: 14,
child: CircularProgressIndicator(
strokeWidth: 2.0,
color: Theme.of(context).colorScheme.primary,
),
)
: repEvent != null
? Text(
repEvent!.text,
style: BotStyle.text(context),
)
: Text(
L10n.of(context)!.oopsSomethingWentWrong,
style: BotStyle.text(context),
),
);
}
}

@ -0,0 +1,119 @@
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/events/message_content.dart';
import 'package:fluffychat/pangea/models/language_model.dart';
import 'package:fluffychat/pangea/models/pangea_message_event.dart';
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.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;
// #Pangea
// final LanguageModel? selectedDisplayLang;
final bool immersionMode;
// final bool definitions;
final bool ownMessage;
final ToolbarDisplayController toolbarController;
final double? width;
// Pangea#
const OverlayMessage(
this.event, {
this.selected = false,
required this.timeline,
// #Pangea
// required this.selectedDisplayLang,
required this.immersionMode,
// required this.definitions,
required this.ownMessage,
required this.toolbarController,
this.width,
// Pangea#
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.surfaceVariant;
final textColor = ownMessage
? Theme.of(context).colorScheme.onPrimaryContainer
: Theme.of(context).colorScheme.onBackground;
final borderRadius = BorderRadius.only(
topLeft: !ownMessage
? const Radius.circular(4)
: const Radius.circular(AppConfig.borderRadius),
topRight: const Radius.circular(AppConfig.borderRadius),
bottomLeft: const Radius.circular(AppConfig.borderRadius),
bottomRight: ownMessage
? const Radius.circular(4)
: const Radius.circular(AppConfig.borderRadius),
);
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.primaryContainer;
}
// #Pangea
final pangeaMessageEvent = PangeaMessageEvent(
event: event,
timeline: timeline,
ownMessage: ownMessage,
);
// Pangea#
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,
),
child: MessageContent(
event,
textColor: textColor,
borderRadius: borderRadius,
selected: selected,
pangeaMessageEvent: pangeaMessageEvent,
// selectedDisplayLang: selectedDisplayLang,
immersionMode: immersionMode,
// definitions: definitions,
toolbarController: toolbarController,
),
),
);
}
}

@ -45,7 +45,6 @@ class _TextToSpeechButtonState extends State<TextToSpeechButton> {
timeline: widget.controller.timeline!,
ownMessage:
widget.selectedEvent.senderId == Matrix.of(context).client.userID,
selected: true,
);
}

@ -1,44 +1,29 @@
import 'dart:developer';
import 'dart:ui';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat/events/html_message.dart';
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
import 'package:fluffychat/pangea/constants/language_keys.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/models/language_model.dart';
import 'package:fluffychat/pangea/models/pangea_message_event.dart';
import 'package:fluffychat/pangea/models/pangea_representation_event.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/utils/show_defintion_util.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 'package:sentry_flutter/sentry_flutter.dart';
import '../../models/pangea_match_model.dart';
import '../../models/pangea_representation_event.dart';
import '../../utils/instructions.dart';
class PangeaRichText extends StatefulWidget {
final PangeaMessageEvent pangeaMessageEvent;
final TextStyle? style;
final bool selected;
final LanguageModel? selectedDisplayLang;
final bool immersionMode;
final bool definitions;
final Choreographer? choreographer;
final ShowDefintionUtil? messageToolbar;
final ToolbarDisplayController toolbarController;
final TextStyle? style;
const PangeaRichText({
super.key,
required this.pangeaMessageEvent,
required this.selected,
required this.selectedDisplayLang,
required this.immersionMode,
required this.definitions,
this.choreographer,
required this.toolbarController,
this.style,
this.messageToolbar,
});
@override
@ -47,93 +32,73 @@ class PangeaRichText extends StatefulWidget {
class PangeaRichTextState extends State<PangeaRichText> {
final PangeaController pangeaController = MatrixState.pangeaController;
RepresentationEvent? repEvent;
bool _fetchingRepresentation = false;
double get blur => _fetchingRepresentation && widget.immersionMode ? 5 : 0;
String textSpan = "";
@override
void initState() {
super.initState();
updateTextSpan();
setTextSpan();
}
@override
void didUpdateWidget(PangeaRichText oldWidget) {
super.didUpdateWidget(oldWidget);
updateTextSpan();
}
Future<void> setTextSpan() async {
setState(() => _fetchingRepresentation = true);
try {
await widget.pangeaMessageEvent.getDisplayRepresentation(context);
} catch (err) {
ErrorHandler.logError(e: err);
}
setState(() => _fetchingRepresentation = false);
void updateTextSpan() {
setState(() {
textSpan = getTextSpan(context);
widget.messageToolbar?.messageText = textSpan;
});
widget.toolbarController.toolbar?.textSelection.setMessageText(
widget.pangeaMessageEvent.displayMessageText,
);
}
@override
Widget build(BuildContext context) {
//TODO - take out of build function of every message
// if (areLanguagesSet) {
if (!widget.selected &&
widget.selectedDisplayLang != null &&
widget.selectedDisplayLang!.langCode != LanguageKeys.unknownLanguage) {
pangeaController.instructions.show(
context,
InstructionsEnum.clickMessage,
widget.pangeaMessageEvent.eventId,
);
} else if (blur > 0) {
pangeaController.instructions.show(
context,
InstructionsEnum.blurMeansTranslate,
widget.pangeaMessageEvent.eventId,
);
}
final Widget richText = widget.pangeaMessageEvent.isHtml
? HtmlMessage(
html: textSpan,
room: widget.pangeaMessageEvent.room,
textColor: widget.style?.color ?? Colors.black,
messageToolbar: widget.messageToolbar,
)
: SelectableText.rich(
onSelectionChanged: (selection, cause) =>
widget.messageToolbar?.onTextSelection(
selectedText: selection,
cause: cause,
context: context,
),
onTap: () => messageToolbar?.onTextTap(context),
focusNode: widget.messageToolbar?.focusNode,
contextMenuBuilder: (context, state) =>
widget.messageToolbar?.contextMenuOverride(
context: context,
textSelection: state,
) ??
const SizedBox(),
TextSpan(
text: textSpan,
style: widget.style,
children: [
if (widget.selected && (_fetchingRepresentation))
const WidgetSpan(
child: Padding(
padding: EdgeInsets.only(left: 5.0),
child: SizedBox(
height: 14,
width: 14,
child: CircularProgressIndicator(
strokeWidth: 2.0,
color: AppConfig.secondaryColor,
),
),
),
final Widget richText = SelectableText.rich(
onSelectionChanged: (selection, cause) => widget
.toolbarController.toolbar?.textSelection
.onTextSelection(selection),
onTap: () => widget.toolbarController.showToolbar(context),
focusNode: widget.toolbarController.focusNode,
contextMenuBuilder: (context, state) =>
MessageContextMenu.contextMenuOverride(
context: context,
textSelection: state,
onDefine: () => widget.toolbarController.showToolbar(
context,
mode: MessageMode.definition,
),
onListen: () => widget.toolbarController.showToolbar(
context,
mode: MessageMode.play,
),
),
TextSpan(
text: widget.pangeaMessageEvent.displayMessageText,
style: widget.style,
children: [
if (_fetchingRepresentation)
const WidgetSpan(
child: Padding(
padding: EdgeInsets.only(left: 5.0),
child: SizedBox(
height: 14,
width: 14,
child: CircularProgressIndicator(
strokeWidth: 2.0,
color: AppConfig.secondaryColor,
),
],
),
),
),
);
],
),
);
return blur > 0
? ImageFiltered(
@ -143,55 +108,6 @@ class PangeaRichTextState extends State<PangeaRichText> {
: richText;
}
String getTextSpan(BuildContext context) {
final String? displayLangCode =
widget.selected ? widget.selectedDisplayLang?.langCode : userL2LangCode;
if (displayLangCode == null || !widget.immersionMode) {
return widget.pangeaMessageEvent.body;
}
if (widget.pangeaMessageEvent.eventId.contains("webdebug")) {
debugger(when: kDebugMode);
return widget.pangeaMessageEvent.body;
}
final RepresentationEvent? repEvent =
widget.pangeaMessageEvent.representationByLanguage(
displayLangCode,
);
if (repEvent == null) {
_fetchingRepresentation = true;
setState(() => {});
widget.pangeaMessageEvent
.representationByLanguageGlobal(
context: context,
langCode: displayLangCode,
)
.onError((error, stackTrace) => ErrorHandler.logError())
.whenComplete(() => setState(() => _fetchingRepresentation = false));
return widget.pangeaMessageEvent.body;
}
if (repEvent.event?.eventId.contains("web") ?? false) {
Sentry.addBreadcrumb(
Breadcrumb.fromJson({"repEvent.event": repEvent.event?.toJson()}),
);
Sentry.addBreadcrumb(
Breadcrumb(
message:
"representationByLanguageGlobal returned RepEvent with event ID containing 'web' - ${repEvent.event?.eventId}",
),
);
}
return widget.pangeaMessageEvent.isHtml
? repEvent.formatBody() ?? repEvent.text
: repEvent.text;
}
bool get areLanguagesSet =>
userL2LangCode != null && userL2LangCode != LanguageKeys.unknownLanguage;

@ -79,6 +79,18 @@ class WordDataCardController extends State<WordDataCard> {
super.initState();
}
@override
void didUpdateWidget(covariant WordDataCard oldWidget) {
if (oldWidget.word != widget.word) {
if (!widget.hasInfo) {
getContextualDefinition();
} else {
getWordNet();
}
}
super.didUpdateWidget(oldWidget);
}
Future<void> getContextualDefinition() async {
ContextualDefinitionRequestModel? req;
try {
@ -89,7 +101,14 @@ class WordDataCardController extends State<WordDataCard> {
fullTextLang: widget.fullTextLang,
wordLang: widget.wordLang,
);
if (mounted) setState(() => isLoadingContextualDefinition = true);
if (!mounted) return;
setState(() {
contextualDefinitionRes = null;
definitionError = null;
isLoadingContextualDefinition = true;
});
contextualDefinitionRes = await controller.definitions.get(req);
if (contextualDefinitionRes == null) {
definitionError = Exception("Error getting definition");
@ -159,54 +178,57 @@ class WordDataCardView extends StatelessWidget {
return Scrollbar(
thumbVisibility: true,
controller: scrollController,
child: SingleChildScrollView(
controller: scrollController,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CardHeader(
text: controller.widget.word,
botExpression: BotExpression.down,
),
if (controller.widget.choiceFeedback != null)
Text(
controller.widget.choiceFeedback!,
style: BotStyle.text(context),
),
const SizedBox(height: 5.0),
if (controller.wordData != null && controller.wordNetError == null)
WordNetInfo(
wordData: controller.wordData!,
activeL1: controller.activeL1!,
activeL2: controller.activeL2!,
child: Expanded(
child: SingleChildScrollView(
controller: scrollController,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CardHeader(
text: controller.widget.word,
botExpression: BotExpression.down,
),
if (controller.isLoadingWordNet) const PCircular(),
const SizedBox(height: 5.0),
// if (controller.widget.hasInfo &&
// !controller.isLoadingContextualDefinition &&
// controller.contextualDefinitionRes == null)
// Material(
// type: MaterialType.transparency,
// child: ListTile(
// leading: const BotFace(
// width: 40, expression: BotExpression.surprised),
// title: Text(L10n.of(context)!.askPangeaBot),
// onTap: controller.handleGetDefinitionButtonPress,
// ),
// ),
if (controller.isLoadingContextualDefinition) const PCircular(),
if (controller.contextualDefinitionRes != null)
Text(
controller.contextualDefinitionRes!.text,
style: BotStyle.text(context),
),
if (controller.definitionError != null)
Text(
L10n.of(context)!.sorryNoResults,
style: BotStyle.text(context),
),
],
if (controller.widget.choiceFeedback != null)
Text(
controller.widget.choiceFeedback!,
style: BotStyle.text(context),
),
const SizedBox(height: 5.0),
if (controller.wordData != null &&
controller.wordNetError == null)
WordNetInfo(
wordData: controller.wordData!,
activeL1: controller.activeL1!,
activeL2: controller.activeL2!,
),
if (controller.isLoadingWordNet) const PCircular(),
const SizedBox(height: 5.0),
// if (controller.widget.hasInfo &&
// !controller.isLoadingContextualDefinition &&
// controller.contextualDefinitionRes == null)
// Material(
// type: MaterialType.transparency,
// child: ListTile(
// leading: const BotFace(
// width: 40, expression: BotExpression.surprised),
// title: Text(L10n.of(context)!.askPangeaBot),
// onTap: controller.handleGetDefinitionButtonPress,
// ),
// ),
if (controller.isLoadingContextualDefinition) const PCircular(),
if (controller.contextualDefinitionRes != null)
Text(
controller.contextualDefinitionRes!.text,
style: BotStyle.text(context),
),
if (controller.definitionError != null)
Text(
L10n.of(context)!.sorryNoResults,
style: BotStyle.text(context),
),
],
),
),
),
);

@ -337,14 +337,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.2.3"
desktop_drop:
dependency: "direct main"
description:
name: desktop_drop
sha256: d55a010fe46c8e8fcff4ea4b451a9ff84a162217bdb3b2a0aa1479776205e15d
url: "https://pub.dev"
source: hosted
version: "0.4.4"
desktop_lifecycle:
dependency: "direct main"
description:

@ -20,7 +20,7 @@ dependencies:
chewie: ^1.7.1
collection: ^1.17.2
cupertino_icons: any
desktop_drop: ^0.4.4
# desktop_drop: ^0.4.4
desktop_notifications: ^0.6.3
device_info_plus: ^9.1.0
dynamic_color: ^1.6.8

Loading…
Cancel
Save