diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index bcd711df5..574be655d 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -5,7 +5,6 @@ import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/room_status_extension.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/hover_builder.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'; @@ -307,71 +306,78 @@ class ChatListItem extends StatelessWidget { maxLines: 1, softWrap: false, ) - : FutureBuilder( - key: ValueKey( - '${lastEvent?.eventId}_${lastEvent?.type}', - ), - // #Pangea - future: room.lastEvent != null - ? GetChatListItemSubtitle().getSubtitle( - L10n.of(context), - room.lastEvent, - MatrixState.pangeaController, - ) - : Future.value( - L10n.of(context).emptyChat, - ), - // future: needLastEventSender - // ? lastEvent.calcLocalizedBody( - // MatrixLocals(L10n.of(context)), - // hideReply: true, - // hideEdit: true, - // plaintextBody: true, - // removeMarkdown: true, - // withSenderNamePrefix: - // (!isDirectChat || - // directChatMatrixId != - // room.lastEvent?.senderId), - // ) - // : null, + // #Pangea + : room.lastEvent != null + ? ChatListItemSubtitle( + event: room.lastEvent, + style: TextStyle( + fontWeight: + unread || room.hasNewMessages + ? FontWeight.bold + : null, + color: + theme.colorScheme.onSurfaceVariant, + ), + ) // Pangea# - initialData: - lastEvent?.calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)), - hideReply: true, - hideEdit: true, - plaintextBody: true, - removeMarkdown: true, - withSenderNamePrefix: (!isDirectChat || - directChatMatrixId != - room.lastEvent?.senderId), - ), - builder: (context, snapshot) => Text( - room.membership == Membership.invite - ? isDirectChat - ? L10n.of(context).invitePrivateChat - // #Pangea - // : L10n.of(context).inviteGroupChat - : L10n.of(context).inviteChat - // Pangea# - : snapshot.data ?? - L10n.of(context).emptyChat, - softWrap: false, - maxLines: - room.notificationCount >= 1 ? 2 : 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontWeight: unread || room.hasNewMessages - ? FontWeight.bold + : FutureBuilder( + key: ValueKey( + '${lastEvent?.eventId}_${lastEvent?.type}', + ), + future: needLastEventSender + ? lastEvent.calcLocalizedBody( + MatrixLocals(L10n.of(context)), + hideReply: true, + hideEdit: true, + plaintextBody: true, + removeMarkdown: true, + withSenderNamePrefix: + (!isDirectChat || + directChatMatrixId != + room.lastEvent + ?.senderId), + ) : null, - color: theme.colorScheme.onSurfaceVariant, - decoration: - room.lastEvent?.redacted == true - ? TextDecoration.lineThrough - : null, + initialData: + lastEvent?.calcLocalizedBodyFallback( + MatrixLocals(L10n.of(context)), + hideReply: true, + hideEdit: true, + plaintextBody: true, + removeMarkdown: true, + withSenderNamePrefix: (!isDirectChat || + directChatMatrixId != + room.lastEvent?.senderId), + ), + builder: (context, snapshot) => Text( + room.membership == Membership.invite + ? isDirectChat + ? L10n.of(context) + .invitePrivateChat + // #Pangea + // : L10n.of(context).inviteGroupChat + : L10n.of(context).inviteChat + // Pangea# + : snapshot.data ?? + L10n.of(context).emptyChat, + softWrap: false, + maxLines: + room.notificationCount >= 1 ? 2 : 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: + unread || room.hasNewMessages + ? FontWeight.bold + : null, + color: theme + .colorScheme.onSurfaceVariant, + decoration: + room.lastEvent?.redacted == true + ? TextDecoration.lineThrough + : null, + ), + ), ), - ), - ), ), const SizedBox(width: 8), AnimatedContainer( diff --git a/lib/pangea/utils/get_chat_list_item_subtitle.dart b/lib/pangea/utils/get_chat_list_item_subtitle.dart index 742934cc2..4f44382a9 100644 --- a/lib/pangea/utils/get_chat_list_item_subtitle.dart +++ b/lib/pangea/utils/get_chat_list_item_subtitle.dart @@ -1,151 +1,119 @@ -import 'package:collection/collection.dart'; -import 'package:fluffychat/pangea/constants/language_constants.dart'; -import 'package:fluffychat/pangea/constants/model_keys.dart'; -import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/models/pangea_token_model.dart'; -import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_token_text.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'; import '../../utils/matrix_sdk_extensions/matrix_locals.dart'; -class GetChatListItemSubtitle { - final List hideContentKeys = [ - ModelKey.transcription, - ]; +class ChatListItemSubtitle extends StatelessWidget { + final Event? event; + final TextStyle style; - String _constructTokens( - List tokens, - List hiddenTokens, - ) { - String result = ""; - int currentPosition = 0; - for (final token in tokens) { - if (token.text.offset > currentPosition) { - result += " " * (token.text.offset - currentPosition); - currentPosition = token.text.offset; - } + const ChatListItemSubtitle({ + super.key, + required this.event, + required this.style, + }); - if (hiddenTokens.contains(token)) { - result += "_" * token.text.length; - } else { - result += token.text.content; - } - currentPosition += token.text.length; - } - return result; + bool _showPangeaContent(Event event) { + return MatrixState.pangeaController.languageController.languagesSet && + !event.redacted && + event.type == EventTypes.Message && + event.messageType == MessageTypes.Text; } - Future getSubtitle( - L10n l10n, - Event? event, - PangeaController pangeaController, + Future _getPangeaMessageEvent( + final Event event, ) async { - if (event == null) return l10n.emptyChat; - try { - if (!pangeaController.languageController.languagesSet || - event.redacted || - event.type != EventTypes.Message || - event.messageType != MessageTypes.Text) { - return event.calcLocalizedBody( - MatrixLocals(l10n), + final Timeline timeline = event.room.timeline != null + ? event.room.timeline! + : await event.room.getTimeline(); + + final pangeaMessageEvent = PangeaMessageEvent( + event: event, + timeline: timeline, + ownMessage: event.senderId == event.room.client.userID, + ); + + final tokens = + await pangeaMessageEvent.messageDisplayRepresentation?.tokensGlobal( + event.senderId, + event.originServerTs, + ); + + return MessageEventAndTokens( + event: pangeaMessageEvent, + tokens: tokens, + ); + } + + @override + Widget build(BuildContext context) { + if (event == null) return Text(L10n.of(context).emptyChat, style: style); + if (!_showPangeaContent(event!)) { + return FutureBuilder( + future: event!.calcLocalizedBody( + MatrixLocals(L10n.of(context)), hideReply: true, hideEdit: true, plaintextBody: true, removeMarkdown: true, - withSenderNamePrefix: !event.room.isDirectChat || - event.room.directChatMatrixID != event.room.lastEvent?.senderId, - ); - } - - String? eventContextId = event.eventId; - if (!event.eventId.isValidMatrixId || event.eventId.sigil != '\$') { - eventContextId = null; - } - - final Timeline timeline = event.room.timeline != null && - event.room.timeline!.chunk.eventsMap.containsKey(eventContextId) - ? event.room.timeline! - : await event.room.getTimeline(eventContextId: eventContextId); - - final PangeaMessageEvent pangeaMessageEvent = PangeaMessageEvent( - event: event, - timeline: timeline, - ownMessage: event.senderId == event.room.client.userID, - ); - - final l2Code = pangeaController.languageController.activeL2Code(); - if (l2Code == null || l2Code == LanguageKeys.unknownLanguage) { - return event.body; - } - - final String? text = - pangeaMessageEvent.messageDisplayRepresentation?.text; - - final tokens = - await pangeaMessageEvent.messageDisplayRepresentation?.tokensGlobal( - event.senderId, - event.originServerTs, - ); - - if (tokens != null) { - final analyticsEntry = pangeaController.getAnalytics.perMessage.get( - tokens, - pangeaMessageEvent, - ); - - if (analyticsEntry?.nextActivity?.activityType == - ActivityTypeEnum.hiddenWordListening) { - try { - return _constructTokens( - tokens, - analyticsEntry!.nextActivity!.tokens, - ); - } catch (err, s) { - ErrorHandler.logError( - e: err, - s: s, - data: { - "tokens": tokens, - "analyticsEntry": analyticsEntry, - }, - ); - } - } - } - - final i18n = MatrixLocals(l10n); - - if (text == null || event.room.lastEvent == null) { - return l10n.emptyChat; - } - - if (!event.room.isDirectChat || - event.room.directChatMatrixID != event.room.lastEvent!.senderId) { - final senderNameOrYou = event.senderId == event.room.client.userID - ? i18n.you - : event.room - .getParticipants() - .firstWhereOrNull((u) => u.id != event.room.client.userID) - ?.calcDisplayname(i18n: i18n) ?? - event.room.lastEvent!.senderId; - - return "$senderNameOrYou: $text"; - } - - return text; - } catch (e, s) { - // debugger(when: kDebugMode); - ErrorHandler.logError( - e: e, - s: s, - data: { - "event": event.toJson(), + withSenderNamePrefix: !event!.room.isDirectChat || + event!.room.directChatMatrixID != event!.room.lastEvent?.senderId, + ), + builder: (context, snapshot) { + return Text( + snapshot.hasData && snapshot.data != null + ? snapshot.data! + : L10n.of(context).emptyChat, + style: style, + ); }, ); - return event.body; } + + return FutureBuilder( + future: _getPangeaMessageEvent(event!), + builder: (context, snapshot) { + if (snapshot.hasData) { + final messageEventAndTokens = snapshot.data as MessageEventAndTokens; + final pangeaMessageEvent = messageEventAndTokens.event; + final tokens = messageEventAndTokens.tokens; + + final analyticsEntry = tokens != null + ? MatrixState.pangeaController.getAnalytics.perMessage.get( + tokens, + pangeaMessageEvent, + ) + : null; + + return MessageTextWidget( + pangeaMessageEvent: pangeaMessageEvent, + style: style, + messageAnalyticsEntry: analyticsEntry, + isSelected: null, + onClick: null, + ); + } + + return Text( + L10n.of(context).emptyChat, + style: style, + ); + }, + ); } } + +class MessageEventAndTokens { + final PangeaMessageEvent event; + final List? tokens; + + MessageEventAndTokens({ + required this.event, + required this.tokens, + }); +} diff --git a/lib/pangea/utils/message_text_util.dart b/lib/pangea/utils/message_text_util.dart new file mode 100644 index 000000000..fd6c7b98e --- /dev/null +++ b/lib/pangea/utils/message_text_util.dart @@ -0,0 +1,63 @@ +import 'package:fluffychat/pangea/controllers/message_analytics_controller.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/models/pangea_token_model.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_token_text.dart'; +import 'package:flutter/material.dart'; + +class MessageTextUtil { + static List getTokenPositions( + PangeaMessageEvent pangeaMessageEvent, { + MessageAnalyticsEntry? messageAnalyticsEntry, + bool Function(PangeaToken)? isSelected, + }) { + // Convert the entire message into a list of characters + final Characters messageCharacters = + pangeaMessageEvent.messageDisplayText.characters; + + // When building token positions, use grapheme cluster indices + final List tokenPositions = []; + int globalIndex = 0; + + for (final token + in pangeaMessageEvent.messageDisplayRepresentation!.tokens!) { + final start = token.start; + final end = token.end; + + // Calculate the number of grapheme clusters up to the start and end positions + final int startIndex = messageCharacters.take(start).length; + final int endIndex = messageCharacters.take(end).length; + + final hideContent = + messageAnalyticsEntry?.isTokenInHiddenWordActivity(token) ?? false; + + final hasHiddenContent = + messageAnalyticsEntry?.hasHiddenWordActivity ?? false; + + if (globalIndex < startIndex) { + tokenPositions.add( + TokenPosition( + start: globalIndex, + end: startIndex, + hideContent: false, + highlight: (isSelected?.call(token) ?? false) && !hasHiddenContent, + ), + ); + } + + tokenPositions.add( + TokenPosition( + start: startIndex, + end: endIndex, + token: token, + hideContent: hideContent, + highlight: (isSelected?.call(token) ?? false) && + !hideContent && + !hasHiddenContent, + ), + ); + globalIndex = endIndex; + } + + return tokenPositions; + } +} diff --git a/lib/pangea/widgets/chat/message_token_text.dart b/lib/pangea/widgets/chat/message_token_text.dart index 6f07e0d63..d80adba9a 100644 --- a/lib/pangea/widgets/chat/message_token_text.dart +++ b/lib/pangea/widgets/chat/message_token_text.dart @@ -2,6 +2,7 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/controllers/message_analytics_controller.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/models/pangea_token_model.dart'; +import 'package:fluffychat/pangea/utils/message_text_util.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -47,110 +48,18 @@ class MessageTokenText extends StatelessWidget { ); } - // Convert the entire message into a list of characters - final Characters messageCharacters = - _pangeaMessageEvent.messageDisplayText.characters; - - // When building token positions, use grapheme cluster indices - final List tokenPositions = []; - int globalIndex = 0; - - for (final token - in _pangeaMessageEvent.messageDisplayRepresentation!.tokens!) { - final start = token.start; - final end = token.end; - - // Calculate the number of grapheme clusters up to the start and end positions - final int startIndex = messageCharacters.take(start).length; - final int endIndex = messageCharacters.take(end).length; - - final hideContent = - messageAnalyticsEntry?.isTokenInHiddenWordActivity(token) ?? false; - - final hasHiddenContent = - messageAnalyticsEntry?.hasHiddenWordActivity ?? false; - - if (globalIndex < startIndex) { - tokenPositions.add( - TokenPosition( - start: globalIndex, - end: startIndex, - hideContent: false, - highlight: (_isSelected?.call(token) ?? false) && !hasHiddenContent, - ), - ); - } - - tokenPositions.add( - TokenPosition( - start: startIndex, - end: endIndex, - token: token, - hideContent: hideContent, - highlight: (_isSelected?.call(token) ?? false) && - !hideContent && - !hasHiddenContent, - ), - ); - globalIndex = endIndex; - } - void callOnClick(TokenPosition tokenPosition) { _onClick != null && tokenPosition.token != null ? _onClick!(tokenPosition.token!) : null; } - return RichText( - text: TextSpan( - children: - tokenPositions.mapIndexed((int i, TokenPosition tokenPosition) { - final substring = messageCharacters - .skip(tokenPosition.start) - .take(tokenPosition.end - tokenPosition.start) - .toString(); - - if (tokenPosition.token != null) { - if (tokenPosition.hideContent) { - return WidgetSpan( - child: GestureDetector( - onTap: () => callOnClick(tokenPosition), - child: HiddenText(text: substring, style: _style), - ), - ); - } - return TextSpan( - recognizer: TapGestureRecognizer() - ..onTap = () => callOnClick(tokenPosition), - text: substring, - style: _style.merge( - TextStyle( - backgroundColor: tokenPosition.highlight - ? Theme.of(context).brightness == Brightness.light - ? Colors.black.withOpacity(0.4) - : Colors.white.withOpacity(0.4) - : Colors.transparent, - ), - ), - ); - } else { - if ((i > 0 || i < tokenPositions.length - 1) && - tokenPositions[i + 1].hideContent && - tokenPositions[i - 1].hideContent) { - return WidgetSpan( - child: GestureDetector( - onTap: () => callOnClick(tokenPosition), - child: HiddenText(text: substring, style: _style), - ), - ); - } - return TextSpan( - text: substring, - style: _style, - ); - } - }).toList(), - ), + return MessageTextWidget( + pangeaMessageEvent: _pangeaMessageEvent, + style: _style, + messageAnalyticsEntry: messageAnalyticsEntry, + isSelected: _isSelected, + onClick: callOnClick, ); } } @@ -213,3 +122,89 @@ class HiddenText extends StatelessWidget { ); } } + +class MessageTextWidget extends StatelessWidget { + final PangeaMessageEvent pangeaMessageEvent; + final TextStyle style; + final MessageAnalyticsEntry? messageAnalyticsEntry; + final bool Function(PangeaToken)? isSelected; + final void Function(TokenPosition tokenPosition)? onClick; + + const MessageTextWidget({ + super.key, + required this.pangeaMessageEvent, + required this.style, + this.messageAnalyticsEntry, + this.isSelected, + this.onClick, + }); + + @override + Widget build(BuildContext context) { + final Characters messageCharacters = + pangeaMessageEvent.messageDisplayText.characters; + + final tokenPositions = MessageTextUtil.getTokenPositions( + pangeaMessageEvent, + messageAnalyticsEntry: messageAnalyticsEntry, + isSelected: isSelected, + ); + + return RichText( + text: TextSpan( + children: + tokenPositions.mapIndexed((int i, TokenPosition tokenPosition) { + final substring = messageCharacters + .skip(tokenPosition.start) + .take(tokenPosition.end - tokenPosition.start) + .toString(); + + if (tokenPosition.token != null) { + if (tokenPosition.hideContent) { + return WidgetSpan( + child: GestureDetector( + onTap: onClick != null + ? () => onClick?.call(tokenPosition) + : null, + child: HiddenText(text: substring, style: style), + ), + ); + } + return TextSpan( + recognizer: TapGestureRecognizer() + ..onTap = + onClick != null ? () => onClick?.call(tokenPosition) : null, + text: substring, + style: style.merge( + TextStyle( + backgroundColor: tokenPosition.highlight + ? Theme.of(context).brightness == Brightness.light + ? Colors.black.withOpacity(0.4) + : Colors.white.withOpacity(0.4) + : Colors.transparent, + ), + ), + ); + } else { + if ((i > 0 || i < tokenPositions.length - 1) && + tokenPositions[i + 1].hideContent && + tokenPositions[i - 1].hideContent) { + return WidgetSpan( + child: GestureDetector( + onTap: onClick != null + ? () => onClick?.call(tokenPosition) + : null, + child: HiddenText(text: substring, style: style), + ), + ); + } + return TextSpan( + text: substring, + style: style, + ); + } + }).toList(), + ), + ); + } +}