toolbar working
This commit is contained in:
parent
57957eac9e
commit
2cb77732e3
35 changed files with 1518 additions and 1210 deletions
|
|
@ -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;
|
||||
// #Pangea
|
||||
// bool dragging = false;
|
||||
|
||||
void onDragEntered(_) => setState(() => dragging = true);
|
||||
// void onDragEntered(_) => setState(() => dragging = true);
|
||||
|
||||
void onDragExited(_) => setState(() => dragging = false);
|
||||
// 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;
|
||||
// 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#
|
||||
// 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
// 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(
|
||||
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,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (controller.room.canSendDefaultMessages &&
|
||||
controller.room.membership == Membership.join)
|
||||
// #Pangea
|
||||
// Container(
|
||||
ConditionalFlexible(
|
||||
isScroll: controller.isRowScrollable,
|
||||
child: ConditionalScroll(
|
||||
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,
|
||||
),
|
||||
bottomRight: Radius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
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,
|
||||
),
|
||||
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,
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.archive_outlined,
|
||||
),
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
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(),
|
||||
onSelectionChanged: (selection, cause) =>
|
||||
messageToolbar?.onTextSelection(
|
||||
selectedText: selection,
|
||||
cause: cause,
|
||||
context: context,
|
||||
),
|
||||
onTap: () => messageToolbar?.onTextTap(context),
|
||||
);
|
||||
},
|
||||
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,
|
||||
),
|
||||
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,
|
||||
),
|
||||
onListen: () => toolbarController?.showToolbar(
|
||||
context,
|
||||
mode: MessageMode.play,
|
||||
),
|
||||
),
|
||||
// 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,
|
||||
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,
|
||||
},
|
||||
},
|
||||
'org.matrix.msc3245.voice': {},
|
||||
'org.matrix.msc1767.audio': {
|
||||
'duration': response.durationMillis,
|
||||
'waveform': response.waveform,
|
||||
},
|
||||
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();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
149
lib/pangea/widgets/chat/message_audio_card.dart
Normal file
149
lib/pangea/widgets/chat/message_audio_card.dart
Normal file
|
|
@ -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,
|
||||
),
|
||||
// ),
|
||||
// ),
|
||||
);
|
||||
}
|
||||
}
|
||||
69
lib/pangea/widgets/chat/message_context_menu.dart
Normal file
69
lib/pangea/widgets/chat/message_context_menu.dart
Normal file
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
34
lib/pangea/widgets/chat/message_text_selection.dart
Normal file
34
lib/pangea/widgets/chat/message_text_selection.dart
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
303
lib/pangea/widgets/chat/message_toolbar.dart
Normal file
303
lib/pangea/widgets/chat/message_toolbar.dart
Normal file
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
103
lib/pangea/widgets/chat/message_translation_card.dart
Normal file
103
lib/pangea/widgets/chat/message_translation_card.dart
Normal file
|
|
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
119
lib/pangea/widgets/chat/overlay_message.dart
Normal file
119
lib/pangea/widgets/chat/overlay_message.dart
Normal file
|
|
@ -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),
|
||||
child: Expanded(
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
CardHeader(
|
||||
text: controller.widget.word,
|
||||
botExpression: BotExpression.down,
|
||||
),
|
||||
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),
|
||||
),
|
||||
],
|
||||
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…
Add table
Reference in a new issue