Merge branch 'main' into learning-analytics-summary
This commit is contained in:
commit
e80e3ba1b8
7 changed files with 161 additions and 109 deletions
|
|
@ -1266,7 +1266,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
|
||||
void pickEmojiReactionAction(Iterable<Event> allReactionEvents) async {
|
||||
// #Pangea
|
||||
MatrixState.pAnyState.closeAllOverlays();
|
||||
closeSelectionOverlay();
|
||||
// Pangea#
|
||||
_allReactionEvents = allReactionEvents;
|
||||
emojiPickerType = EmojiPickerType.reaction;
|
||||
|
|
@ -1287,9 +1287,19 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
// Pangea#
|
||||
}
|
||||
|
||||
// #Pangea
|
||||
/// Close the combined selection view overlay and clear the message
|
||||
/// text and selection stored for the text in that overlay
|
||||
void closeSelectionOverlay() {
|
||||
MatrixState.pAnyState.closeAllOverlays();
|
||||
textSelection.clearMessageText();
|
||||
textSelection.onSelection(null);
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
void clearSelectedEvents() => setState(() {
|
||||
// #Pangea
|
||||
MatrixState.pAnyState.closeAllOverlays();
|
||||
closeSelectionOverlay();
|
||||
// Pangea#
|
||||
selectedEvents.clear();
|
||||
showEmojiPicker = false;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.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';
|
||||
|
|
@ -18,12 +21,22 @@ class HtmlMessage extends StatelessWidget {
|
|||
final String html;
|
||||
final Room room;
|
||||
final Color textColor;
|
||||
// #Pangea
|
||||
final bool isOverlay;
|
||||
final PangeaMessageEvent? pangeaMessageEvent;
|
||||
final ChatController controller;
|
||||
// Pangea#
|
||||
|
||||
const HtmlMessage({
|
||||
super.key,
|
||||
required this.html,
|
||||
required this.room,
|
||||
this.textColor = Colors.black,
|
||||
// #Pangea
|
||||
required this.isOverlay,
|
||||
this.pangeaMessageEvent,
|
||||
required this.controller,
|
||||
// Pangea#
|
||||
});
|
||||
|
||||
dom.Node _linkifyHtml(dom.Node element) {
|
||||
|
|
@ -58,6 +71,9 @@ class HtmlMessage extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// #Pangea
|
||||
controller.textSelection.setMessageText(html);
|
||||
// Pangea#
|
||||
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
|
||||
|
||||
final linkColor = textColor.withAlpha(150);
|
||||
|
|
@ -76,21 +92,16 @@ 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,
|
||||
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(),
|
||||
return SelectionArea(
|
||||
onSelectionChanged: (SelectedContent? selection) {
|
||||
controller.textSelection.onSelection(selection?.plainText);
|
||||
},
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (pangeaMessageEvent != null && !isOverlay) {
|
||||
controller.showToolbar(pangeaMessageEvent!);
|
||||
}
|
||||
},
|
||||
// Pangea#
|
||||
child: Html.fromElement(
|
||||
documentElement: element as dom.Element,
|
||||
|
|
@ -173,11 +184,6 @@ class HtmlMessage extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
);
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
}
|
||||
|
||||
static const Set<String> fallbackTextTags = {'tg-forward'};
|
||||
|
|
@ -303,7 +309,6 @@ class ImageExtension extends HtmlExtension {
|
|||
uri: mxcUrl,
|
||||
width: width ?? height ?? defaultDimension,
|
||||
height: height ?? width ?? defaultDimension,
|
||||
cacheKey: mxcUrl.toString(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ import 'dart:math';
|
|||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/events/video_player.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/pangea_rich_text.dart';
|
||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
|
@ -205,6 +205,11 @@ class MessageContent extends StatelessWidget {
|
|||
html: html,
|
||||
textColor: textColor,
|
||||
room: event.room,
|
||||
// #Pangea
|
||||
isOverlay: isOverlay,
|
||||
controller: controller,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
// Pangea#
|
||||
);
|
||||
}
|
||||
// else we fall through to the normal message rendering
|
||||
|
|
@ -285,8 +290,8 @@ class MessageContent extends StatelessWidget {
|
|||
final bigEmotes = event.onlyEmotes &&
|
||||
event.numberEmotes > 0 &&
|
||||
event.numberEmotes <= 10;
|
||||
|
||||
// #Pangea
|
||||
// return Linkify(
|
||||
final messageTextStyle = TextStyle(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: textColor,
|
||||
|
|
@ -314,38 +319,36 @@ class MessageContent extends StatelessWidget {
|
|||
),
|
||||
);
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
return SelectableLinkify(
|
||||
onSelectionChanged: (selection, cause) {
|
||||
if (isOverlay) {
|
||||
controller.textSelection.onTextSelection(selection);
|
||||
}
|
||||
},
|
||||
onTap: () {
|
||||
if (pangeaMessageEvent != null && !isOverlay) {
|
||||
HapticFeedback.mediumImpact();
|
||||
controller.showToolbar(pangeaMessageEvent!);
|
||||
}
|
||||
},
|
||||
enableInteractiveSelection: isOverlay,
|
||||
// Pangea#
|
||||
text: event.calcLocalizedBodyFallback(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
hideReply: true,
|
||||
return
|
||||
// #Pangea
|
||||
ToolbarSelectionArea(
|
||||
controller: controller,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
isOverlay: isOverlay,
|
||||
child:
|
||||
// Pangea#
|
||||
Linkify(
|
||||
text: 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(),
|
||||
),
|
||||
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(),
|
||||
);
|
||||
}
|
||||
case EventTypes.CallInvite:
|
||||
|
|
|
|||
|
|
@ -1,37 +1,41 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// Contains information about the text currently being shown in a
|
||||
/// toolbar overlay message and any selection within that text.
|
||||
/// The ChatController contains one instance of this class, and it's values
|
||||
/// should be updated each time an overlay is openned or closed, or when
|
||||
/// an overlay's text selection changes.
|
||||
class MessageTextSelection {
|
||||
/// The currently selected text in the overlay message.
|
||||
String? selectedText;
|
||||
String messageText = "";
|
||||
|
||||
/// The full text displayed in the overlay message.
|
||||
String? messageText;
|
||||
|
||||
/// A stream that emits the currently selected text whenever it changes.
|
||||
final StreamController<String?> selectionStream =
|
||||
StreamController<String?>.broadcast();
|
||||
|
||||
void setMessageText(String text) {
|
||||
messageText = text;
|
||||
}
|
||||
/// Sets messageText to match the text currently being displayed in the overlay.
|
||||
/// Text in messages is displayed in a variety of ways, i.e., direct message content,
|
||||
/// translation, HTML rendered message, etc. This method should be called wherever the
|
||||
/// text displayed in the overlay is determined.
|
||||
void setMessageText(String text) => messageText = text;
|
||||
|
||||
void onTextSelection(TextSelection selection) => selection.isCollapsed == true
|
||||
? clearTextSelection()
|
||||
: setTextSelection(selection);
|
||||
/// Clears the messageText value. Called when the message selection overlay is closed.
|
||||
void clearMessageText() => messageText = null;
|
||||
|
||||
void setTextSelection(TextSelection selection) {
|
||||
selectedText = selection.textInside(messageText);
|
||||
if (BrowserContextMenu.enabled && kIsWeb) {
|
||||
BrowserContextMenu.disableContextMenu();
|
||||
}
|
||||
/// Updates the selectedText value and emits it to the selectionStream.
|
||||
void onSelection(String? text) {
|
||||
text == null || text.isEmpty ? selectedText = null : selectedText = text;
|
||||
selectionStream.add(selectedText);
|
||||
}
|
||||
|
||||
void clearTextSelection() {
|
||||
selectedText = null;
|
||||
if (kIsWeb && !BrowserContextMenu.enabled) {
|
||||
BrowserContextMenu.enableContextMenu();
|
||||
}
|
||||
selectionStream.add(selectedText);
|
||||
/// Returns the index of the selected text within the message text.
|
||||
/// If the selected text is not found, returns null.
|
||||
int? get offset {
|
||||
if (selectedText == null || messageText == null) return null;
|
||||
final index = messageText!.indexOf(selectedText!);
|
||||
return index > -1 ? index : null;
|
||||
}
|
||||
|
||||
int get offset => messageText.indexOf(selectedText!);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart';
|
|||
import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class MessageToolbar extends StatefulWidget {
|
||||
final MessageTextSelection textSelection;
|
||||
|
|
@ -140,6 +141,7 @@ class MessageToolbarState extends State<MessageToolbar> {
|
|||
void showDefinition() {
|
||||
debugPrint("show definition");
|
||||
if (widget.textSelection.selectedText == null ||
|
||||
widget.textSelection.messageText == null ||
|
||||
widget.textSelection.selectedText!.isEmpty) {
|
||||
toolbarContent = const SelectToDefine();
|
||||
return;
|
||||
|
|
@ -148,7 +150,7 @@ class MessageToolbarState extends State<MessageToolbar> {
|
|||
toolbarContent = WordDataCard(
|
||||
word: widget.textSelection.selectedText!,
|
||||
wordLang: widget.pangeaMessageEvent.messageDisplayLangCode,
|
||||
fullText: widget.textSelection.messageText,
|
||||
fullText: widget.textSelection.messageText!,
|
||||
fullTextLang: widget.pangeaMessageEvent.messageDisplayLangCode,
|
||||
hasInfo: true,
|
||||
room: widget.controller.room,
|
||||
|
|
@ -276,3 +278,35 @@ class MessageToolbarState extends State<MessageToolbar> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarSelectionArea extends StatelessWidget {
|
||||
final ChatController controller;
|
||||
final PangeaMessageEvent? pangeaMessageEvent;
|
||||
final bool isOverlay;
|
||||
final Widget child;
|
||||
|
||||
const ToolbarSelectionArea({
|
||||
required this.controller,
|
||||
this.pangeaMessageEvent,
|
||||
this.isOverlay = false,
|
||||
required this.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SelectionArea(
|
||||
onSelectionChanged: (SelectedContent? selection) {
|
||||
controller.textSelection.onSelection(selection?.plainText);
|
||||
},
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (pangeaMessageEvent != null && !isOverlay) {
|
||||
controller.showToolbar(pangeaMessageEvent!);
|
||||
}
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,8 @@ class MessageTranslationCardState extends State<MessageTranslationCard> {
|
|||
Future<void> translateSelection() async {
|
||||
if (widget.selection.selectedText == null ||
|
||||
l1Code == null ||
|
||||
l2Code == null) {
|
||||
l2Code == null ||
|
||||
widget.selection.messageText == null) {
|
||||
selectionTranslation = null;
|
||||
return;
|
||||
}
|
||||
|
|
@ -64,7 +65,7 @@ class MessageTranslationCardState extends State<MessageTranslationCard> {
|
|||
final resp = await FullTextTranslationRepo.translate(
|
||||
accessToken: accessToken,
|
||||
request: FullTextTranslationRequestModel(
|
||||
text: widget.selection.messageText,
|
||||
text: widget.selection.messageText!,
|
||||
tgtLang: l1Code!,
|
||||
userL1: l1Code!,
|
||||
userL2: l2Code!,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import 'package:fluffychat/pangea/enum/instructions_enum.dart';
|
|||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -134,37 +135,31 @@ class PangeaRichTextState extends State<PangeaRichText> {
|
|||
}
|
||||
|
||||
//TODO - take out of build function of every message
|
||||
final Widget richText = SelectableText.rich(
|
||||
onSelectionChanged: (selection, cause) {
|
||||
if (widget.isOverlay) {
|
||||
widget.controller.textSelection.onTextSelection(selection);
|
||||
}
|
||||
},
|
||||
onTap: () {
|
||||
if (!widget.isOverlay) {
|
||||
widget.controller.showToolbar(widget.pangeaMessageEvent);
|
||||
}
|
||||
},
|
||||
enableInteractiveSelection: widget.isOverlay,
|
||||
TextSpan(
|
||||
text: textSpan,
|
||||
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,
|
||||
final Widget richText = ToolbarSelectionArea(
|
||||
isOverlay: widget.isOverlay,
|
||||
pangeaMessageEvent: widget.pangeaMessageEvent,
|
||||
controller: widget.controller,
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: textSpan,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue