refactor: separate token and message reading assistance modes (#2416)

* refactor: separate token and message reading assistance modes

* chore: apply same token styling to HTML formatted messages

* chore: don't wait for lemma responses before showing reading assistance content
This commit is contained in:
ggurdin 2025-04-10 16:51:42 -04:00 committed by GitHub
parent 184442a5ff
commit b7a6ee6fe2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 766 additions and 453 deletions

View file

@ -27,10 +27,10 @@ abstract class AppConfig {
static const double toolbarMinHeight = 200.0;
static const double toolbarMinWidth = 350.0;
static const double defaultHeaderHeight = 56.0;
static const double readingAssistanceInputBarHeight = 170;
static const double toolbarButtonsHeight = 50.0;
static const double toolbarSpacing = 8.0;
static const double toolbarIconSize = 24.0;
static const double readingAssistanceInputBarHeight = 140.0;
static TextStyle messageTextStyle(
Event? event,

View file

@ -10,10 +10,16 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/common/utils/any_state_holder.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
import 'package:fluffychat/pangea/message_token_text/message_token_button.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/utils/token_rendering_util.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/mxc_image.dart';
import '../../../utils/url_launcher.dart';
@ -25,12 +31,14 @@ class HtmlMessage extends StatelessWidget {
final TextStyle linkStyle;
final void Function(LinkableElement) onOpen;
// #Pangea
final bool isOverlay;
final MessageOverlayController? overlayController;
final PangeaMessageEvent? pangeaMessageEvent;
final ChatController controller;
final Event event;
final Event? nextEvent;
final Event? prevEvent;
final bool isTransitionAnimation;
final ReadingAssistanceMode? readingAssistanceMode;
final bool Function(PangeaToken)? isSelected;
final void Function(PangeaToken)? onClick;
@ -45,7 +53,7 @@ class HtmlMessage extends StatelessWidget {
this.textColor = Colors.black,
required this.onOpen,
// #Pangea
required this.isOverlay,
this.overlayController,
required this.event,
this.pangeaMessageEvent,
required this.controller,
@ -53,6 +61,8 @@ class HtmlMessage extends StatelessWidget {
this.prevEvent,
this.isSelected,
this.onClick,
this.isTransitionAnimation = false,
this.readingAssistanceMode,
// Pangea#
});
@ -262,53 +272,91 @@ class HtmlMessage extends StatelessWidget {
? isSelected!.call(token)
: false;
final shouldDo = pangeaMessageEvent?.shouldDoActivity(
token: token,
a: ActivityTypeEnum.wordMeaning,
feature: null,
tag: null,
) ??
false;
final renderer = TokenRenderingUtil(
pangeaMessageEvent: pangeaMessageEvent,
readingAssistanceMode: readingAssistanceMode,
existingStyle: AppConfig.messageTextStyle(
pangeaMessageEvent!.event,
textColor,
),
overlayController: overlayController,
isTransitionAnimation: isTransitionAnimation,
);
// @ggurdin: probably changing this, not sure when it shows up
final didMeaningActivity = token?.didActivitySuccessfully(
ActivityTypeEnum.wordMeaning,
) ??
true;
Color backgroundColor = Colors.transparent;
if (selected) {
backgroundColor = Theme.of(context).colorScheme.primary.withAlpha(80);
} else if (isSelected != null && shouldDo) {
backgroundColor = !didMeaningActivity
? AppConfig.success.withAlpha(60)
: AppConfig.gold.withAlpha(60);
}
final tokenWidth = renderer.tokenTextWidthForContainer(
context,
node.innerHtml,
);
return WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: onClick != null && token != null
? () => onClick?.call(token)
child: CompositedTransformTarget(
link: token != null && renderer.assignTokenKey
? MatrixState.pAnyState
.layerLinkAndKey(token.text.uniqueKey)
.link
: LayerLinkAndKey(token.hashCode.toString()).link,
child: Column(
key: token != null && renderer.assignTokenKey
? MatrixState.pAnyState
.layerLinkAndKey(token.text.uniqueKey)
.key
: null,
child: Text.rich(
TextSpan(
children: [
LinkifySpan(
text: node.innerHtml,
style: AppConfig.messageTextStyle(
pangeaMessageEvent!.event,
textColor,
).merge(TextStyle(backgroundColor: backgroundColor)),
linkStyle: linkStyle,
onOpen: (url) =>
UrlLauncher(context, url.url).launchUrl(),
children: [
if (renderer.showCenterStyling && token != null)
MessageTokenButton(
token: token,
overlayController: overlayController,
textStyle: renderer.style(
context,
color: renderer.backgroundColor(
context,
selected,
),
),
],
width: tokenWidth,
animate: isTransitionAnimation,
practiceTarget:
overlayController?.toolbarMode.associatedActivityType !=
null
? overlayController?.practiceSelection
?.activities(
overlayController!
.toolbarMode.associatedActivityType!,
)
.firstWhereOrNull(
(a) => a.tokens.contains(token),
)
: null,
),
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: onClick != null && token != null
? () => onClick?.call(token)
: null,
child: Text.rich(
TextSpan(
children: [
LinkifySpan(
text: node.innerHtml,
style: renderer.style(
context,
color: renderer.backgroundColor(
context,
selected,
),
),
linkStyle: linkStyle,
onOpen: (url) =>
UrlLauncher(context, url.url).launchUrl(),
),
],
),
),
),
),
),
],
),
),
);
@ -611,7 +659,7 @@ class HtmlMessage extends StatelessWidget {
return SelectionArea(
child: GestureDetector(
onTap: () {
if (!isOverlay) {
if (overlayController == null) {
controller.showToolbar(
pangeaMessageEvent?.event ?? event,
pangeaMessageEvent: pangeaMessageEvent,

View file

@ -13,6 +13,7 @@ import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dar
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_token_text.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_toolbar_selection_area.dart';
@ -44,6 +45,7 @@ class MessageContent extends StatelessWidget {
final Event? nextEvent;
final Event? prevEvent;
final bool isTransitionAnimation;
final ReadingAssistanceMode? readingAssistanceMode;
// Pangea#
final Timeline timeline;
@ -61,6 +63,7 @@ class MessageContent extends StatelessWidget {
this.nextEvent,
this.prevEvent,
this.isTransitionAnimation = false,
this.readingAssistanceMode,
// Pangea#
required this.linkColor,
required this.borderRadius,
@ -256,13 +259,15 @@ class MessageContent extends StatelessWidget {
room: event.room,
// #Pangea
event: event,
isOverlay: overlayController != null,
overlayController: overlayController,
controller: controller,
pangeaMessageEvent: pangeaMessageEvent,
nextEvent: nextEvent,
prevEvent: prevEvent,
isSelected: overlayController != null ? isSelected : null,
onClick: event.isActivityMessage ? null : onClick,
isTransitionAnimation: isTransitionAnimation,
readingAssistanceMode: readingAssistanceMode,
// Pangea#
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
linkStyle: TextStyle(
@ -394,6 +399,7 @@ class MessageContent extends StatelessWidget {
true,
overlayController: overlayController,
isTransitionAnimation: isTransitionAnimation,
readingAssistanceMode: readingAssistanceMode,
);
}

View file

@ -53,9 +53,11 @@ class ReactionsPicker extends StatelessWidget {
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
color: theme.colorScheme.onInverseSurface,
borderRadius: const BorderRadius.only(
decoration: const BoxDecoration(
// #Pangea
// color: theme.colorScheme.onInverseSurface,
// Pangea#
borderRadius: BorderRadius.only(
bottomRight: Radius.circular(AppConfig.borderRadius),
),
),

View file

@ -103,6 +103,10 @@ class PangeaAnyState {
RenderBox? getRenderBox(String key) =>
layerLinkAndKey(key).key.currentContext?.findRenderObject() as RenderBox?;
bool isOverlayOpen(String overlayKey) {
return entries.any((element) => element.key == overlayKey);
}
}
class LayerLinkAndKey {

View file

@ -95,10 +95,12 @@ class _CustomizedSvgState extends State<CustomizedSvg> {
_hasError = modifiedSvg == null;
});
} catch (_) {
setState(() {
_isLoading = false;
_hasError = true;
});
if (mounted) {
setState(() {
_isLoading = false;
_hasError = true;
});
}
}
}

View file

@ -18,10 +18,10 @@ class TokenPosition {
/// End index of the token in the message
final int tokenEnd;
final bool selected;
final bool hideContent;
final PangeaToken? token;
final bool isHighlighted;
final bool selected;
const TokenPosition({
required this.start,
@ -36,6 +36,8 @@ class TokenPosition {
}
class MessageTextUtil {
static final Map<String, List<TokenPosition>> _tokenPositionsCache = {};
static List<TokenPosition>? getTokenPositions(
PangeaMessageEvent pangeaMessageEvent, {
PracticeSelection? messageAnalyticsEntry,
@ -47,6 +49,27 @@ class MessageTextUtil {
return null;
}
if (_tokenPositionsCache.containsKey(pangeaMessageEvent.eventId)) {
return _tokenPositionsCache[pangeaMessageEvent.eventId]!
.map(
(t) => TokenPosition(
start: t.start,
end: t.end,
tokenStart: t.tokenStart,
tokenEnd: t.tokenEnd,
hideContent: t.hideContent,
selected: t.token != null
? isSelected?.call(t.token!) ?? false
: false,
isHighlighted: t.token != null
? isHighlighted?.call(t.token!) ?? false
: false,
token: t.token,
),
)
.toList();
}
// Convert the entire message into a list of characters
final Characters messageCharacters =
pangeaMessageEvent.messageDisplayText.characters;
@ -131,6 +154,7 @@ class MessageTextUtil {
continue;
}
_tokenPositionsCache[pangeaMessageEvent.eventId] = tokenPositions;
return tokenPositions;
} catch (err, s) {
ErrorHandler.logError(

View file

@ -41,6 +41,18 @@ enum MorphFeaturesEnum {
Unknown,
}
class MorphFeatureUtil {
static final Map<String, MorphFeaturesEnum> _morphFeatureCache = {};
static void set(String key, MorphFeaturesEnum value) {
_morphFeatureCache[key] = value;
}
static MorphFeaturesEnum? get(String key) {
return _morphFeatureCache[key];
}
}
extension MorphFeaturesEnumExtension on MorphFeaturesEnum {
/// Convert enum to string
String toShortString() {
@ -49,6 +61,12 @@ extension MorphFeaturesEnumExtension on MorphFeaturesEnum {
/// Convert string to enum
static MorphFeaturesEnum fromString(String category) {
// Repeated regex operations are causing performance issues,
// so we cache the results in a static map
if (MorphFeatureUtil.get(category) != null) {
return MorphFeatureUtil.get(category)!;
}
final morph = MorphFeaturesEnum.values.firstWhereOrNull(
(e) =>
e.toShortString() ==
@ -62,6 +80,8 @@ extension MorphFeaturesEnumExtension on MorphFeaturesEnum {
);
return MorphFeaturesEnum.Unknown;
}
MorphFeatureUtil.set(category, morph);
return morph;
}

View file

@ -0,0 +1,10 @@
enum ReadingAssistanceMode {
/// Overlay message is directly over the original message
tokenMode,
/// Overlay message is centered and larger than the original message
messageMode,
/// Overlay message is moving to the center of the screen
transitionMode,
}

View file

@ -33,8 +33,12 @@ class OverlayFooter extends StatelessWidget {
// constraints: const BoxConstraints(
// maxWidth: FluffyThemes.columnWidth * 2.5,
// ),
height: AppConfig.readingAssistanceInputBarHeight +
AppConfig.toolbarButtonsHeight +
20.0,
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (showToolbarButtons)
ToolbarButtonRow(overlayController: overlayController),

View file

@ -4,6 +4,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/reactions_picker.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_mode_locked_card.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
@ -48,7 +49,7 @@ class ReadingAssistanceInputBar extends StatelessWidget {
case MessageMode.noneSelected:
case MessageMode.messageMeaning:
//TODO: show all emojis for the lemmas and allow sending normal reactions
break;
return ReactionsPicker(controller);
case MessageMode.messageTranslation:
if (overlayController.isTranslationUnlocked) {
@ -97,10 +98,6 @@ class ReadingAssistanceInputBar extends StatelessWidget {
}
}
if (content == null) {
return const SizedBox();
}
return Container(
constraints: const BoxConstraints(
minHeight: minContentHeight,
@ -114,8 +111,7 @@ class ReadingAssistanceInputBar extends StatelessWidget {
return Expanded(
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: (MediaQuery.of(context).size.height / 2) -
AppConfig.toolbarButtonsHeight,
maxHeight: AppConfig.readingAssistanceInputBarHeight,
maxWidth: overlayController.maxWidth,
),
child: AnimatedSize(

View file

@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
class TokenRenderingUtil {
final PangeaMessageEvent? pangeaMessageEvent;
final ReadingAssistanceMode? readingAssistanceMode;
final MessageOverlayController? overlayController;
final bool isTransitionAnimation;
final TextStyle existingStyle;
static final Map<String, double> _tokensWidthCache = {};
TokenRenderingUtil({
required this.pangeaMessageEvent,
required this.readingAssistanceMode,
required this.existingStyle,
this.overlayController,
this.isTransitionAnimation = false,
});
bool get showCenterStyling {
if (overlayController == null) return false;
if (!isTransitionAnimation) return true;
return readingAssistanceMode == ReadingAssistanceMode.transitionMode;
}
double? _fontSize(BuildContext context) => showCenterStyling
? overlayController != null && overlayController!.maxWidth > 600
? Theme.of(context).textTheme.titleLarge?.fontSize
: Theme.of(context).textTheme.bodyLarge?.fontSize
: null;
TextStyle style(
BuildContext context, {
Color? color,
}) =>
existingStyle.copyWith(
fontSize: _fontSize(context),
decoration: TextDecoration.underline,
decorationThickness: 4,
decorationColor: color ?? Colors.white.withAlpha(0),
);
double tokenTextWidthForContainer(BuildContext context, String text) {
final tokenSizeKey = "$text-${_fontSize(context)}";
if (_tokensWidthCache.containsKey(tokenSizeKey)) {
return _tokensWidthCache[tokenSizeKey]!;
}
final textPainter = TextPainter(
text: TextSpan(
text: text,
style: style(context),
),
maxLines: 1,
textDirection: TextDirection.ltr,
)..layout();
final width = textPainter.width;
textPainter.dispose();
_tokensWidthCache[tokenSizeKey] = width;
return width;
}
// Only one token on the screen can have the token's unique key at a time.
// When readingAssistanceMode is not null, there are two messages - the centered message and the transition message.
// When in word mode, the key goes to the transition message.
// If actively transitioning, neither gets the keys.
// If in message mode, the key goes to the centered message (isTransitionAnimation == false).
bool get assignTokenKey {
if (readingAssistanceMode == null) {
return false;
}
switch (readingAssistanceMode!) {
case ReadingAssistanceMode.tokenMode:
return isTransitionAnimation;
case ReadingAssistanceMode.transitionMode:
return false;
case ReadingAssistanceMode.messageMode:
return !isTransitionAnimation;
}
}
Color backgroundColor(BuildContext context, bool selected) {
return selected
? Theme.of(context).colorScheme.primary
: Colors.white.withAlpha(0);
}
}

View file

@ -142,16 +142,22 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
.messageDisplayRepresentation!.tokensToSave
.map((e) => e.vocabConstructID)
.toList();
final List<Future<LemmaInfoResponse>> lemmaInfoFutures =
messageVocabConstructIds
.map((token) => token.getLemmaInfo())
.toList();
final List<LemmaInfoResponse> lemmaInfos =
await Future.wait(lemmaInfoFutures);
messageLemmaInfos = Map.fromIterables(
messageVocabConstructIds,
lemmaInfos,
);
Future.wait(lemmaInfoFutures).then((resp) {
if (mounted) {
setState(
() => messageLemmaInfos = Map.fromIterables(
messageVocabConstructIds,
resp,
),
);
}
});
} catch (e, s) {
debugger(when: kDebugMode);
ErrorHandler.logError(
@ -285,28 +291,38 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
_selectedSpan = selectedSpan;
}
if (selectedToken != null) {
final entry = ReadingAssistanceContent(
key: wordZoomKey,
pangeaMessageEvent: pangeaMessageEvent!,
overlayController: this,
);
if (mounted) {
OverlayUtil.showPositionedCard(
context: context,
cardToShow: entry,
transformTargetId: selectedToken!.text.uniqueKey,
closePrevOverlay: false,
backDropToDismiss: false,
addBorder: false,
overlayKey: selectedToken!.text.uniqueKey,
maxHeight: AppConfig.toolbarMaxHeight,
maxWidth: AppConfig.toolbarMinWidth,
);
}
if (mounted) setState(() {});
Future.delayed(const Duration(milliseconds: 10), () {
_showReadingAssistanceContent();
});
}
void _showReadingAssistanceContent() {
if (selectedToken == null) return;
if (MatrixState.pAnyState.isOverlayOpen(
selectedToken!.text.uniqueKey,
)) {
return;
}
if (mounted) setState(() {});
final entry = ReadingAssistanceContent(
key: wordZoomKey,
pangeaMessageEvent: pangeaMessageEvent!,
overlayController: this,
);
if (mounted) {
OverlayUtil.showPositionedCard(
context: context,
cardToShow: entry,
transformTargetId: selectedToken!.text.uniqueKey,
closePrevOverlay: false,
backDropToDismiss: false,
addBorder: false,
overlayKey: selectedToken!.text.uniqueKey,
maxHeight: AppConfig.toolbarMaxHeight,
maxWidth: AppConfig.toolbarMinWidth,
);
}
}
void updateToolbarMode(MessageMode mode) => setState(() {

View file

@ -15,6 +15,7 @@ import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/overlay_footer.dart';
import 'package:fluffychat/pangea/toolbar/widgets/measure_render_box.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
@ -53,31 +54,36 @@ class MessageSelectionPositioner extends StatefulWidget {
class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
with TickerProviderStateMixin {
late AnimationController _animationController;
Animation<Offset>? _overlayOffsetAnimation;
Animation<Size>? _messageSizeAnimation;
StreamSubscription? _reactionSubscription;
Offset? _centeredMessageOffset;
Size? _centeredMessageSize;
Size? _tooltipSize;
Size? _inputBarSize;
final Completer _centeredMessageCompleter = Completer();
final Completer _tooltipCompleter = Completer();
bool _finishedAnimation = false;
MessageMode _currentMode = MessageMode.noneSelected;
ReadingAssistanceMode? _readingAssistanceMode;
Animation<Offset>? _overlayOffsetAnimation;
Animation<Size>? _messageSizeAnimation;
Offset? _currentOffset;
StreamSubscription? _reactionSubscription;
final _animationDuration = const Duration(
milliseconds: AppConfig.overlayAnimationDuration,
// seconds: 5,
);
@override
void initState() {
super.initState();
_currentMode = widget.overlayController.toolbarMode;
_animationController = AnimationController(
vsync: this,
duration: const Duration(
milliseconds: AppConfig.overlayAnimationDuration,
// seconds: 5,
),
duration: _animationDuration,
);
_reactionSubscription =
@ -100,26 +106,37 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
},
).listen((_) => setState(() {}));
Future.wait([
_centeredMessageCompleter.future,
if (showToolbarButtons) _tooltipCompleter.future,
]).then((_) => _startAnimation());
WidgetsBinding.instance.addPostFrameCallback((_) async {
await _centeredMessageCompleter.future;
if (!mounted) return;
setState(() {
_currentOffset = Offset(
_ownMessage ? _messageRightOffset : _messageLeftOffset,
_originalMessageBottomOffset - _reactionsHeight,
);
});
_setReadingAssistanceMode(
widget.initialSelectedToken == null
? ReadingAssistanceMode.messageMode
: ReadingAssistanceMode.tokenMode,
);
});
}
@override
void didUpdateWidget(MessageSelectionPositioner oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.overlayController.toolbarMode !=
widget.overlayController.toolbarMode) {
setState(() {});
final mode = widget.overlayController.toolbarMode;
if (mode != _currentMode) {
if (_currentMode == MessageMode.noneSelected) {
_setReadingAssistanceMode(ReadingAssistanceMode.messageMode);
}
setState(() => _currentMode = mode);
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
}
@override
void dispose() {
_animationController.dispose();
@ -154,46 +171,68 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
}
}
void _setInputBarSize(RenderBox renderBox) {
setState(() => _inputBarSize = renderBox.size);
}
void _startAnimation() {
if (_mediaQuery == null) {
Future<void> _setReadingAssistanceMode(ReadingAssistanceMode mode) async {
if (mode == _readingAssistanceMode) {
return;
}
_overlayOffsetAnimation = Tween<Offset>(
begin: Offset(
_ownMessage ? _messageRightOffset : _messageLeftOffset,
_messageBottomOffset - _reactionsHeight,
),
// For own messages, dx is the right offset. For other's messages, dx is the left offset.
end: _adjustedCenteredMessageOffset,
).animate(
CurvedAnimation(
parent: _animationController,
curve: FluffyThemes.animationCurve,
),
);
await _centeredMessageCompleter.future;
_messageSizeAnimation = Tween<Size>(
begin: Size(
_messageSize.width,
_originalMessageHeight,
),
end: _adjustedCenteredMessageSize,
).animate(
CurvedAnimation(
parent: _animationController,
curve: FluffyThemes.animationCurve,
),
);
if (mode == ReadingAssistanceMode.messageMode) {
setState(
() => _readingAssistanceMode = ReadingAssistanceMode.transitionMode,
);
} else if (mode == ReadingAssistanceMode.tokenMode) {
setState(
() => _readingAssistanceMode = ReadingAssistanceMode.tokenMode,
);
}
_animationController.forward().then((_) {
_finishedAnimation = true;
if (mounted) setState(() {});
});
if (mode == ReadingAssistanceMode.tokenMode) {
_overlayOffsetAnimation = Tween<Offset>(
begin: _currentOffset,
end: _adjustedOriginalMessageOffset,
).animate(
CurvedAnimation(
parent: _animationController,
curve: FluffyThemes.animationCurve,
),
)..addListener(() {
if (mounted) {
setState(() => _currentOffset = _overlayOffsetAnimation?.value);
}
});
} else if (mode == ReadingAssistanceMode.messageMode) {
_overlayOffsetAnimation = Tween<Offset>(
begin: _currentOffset,
end: _centeredMessageOffset!,
).animate(
CurvedAnimation(
parent: _animationController,
curve: FluffyThemes.animationCurve,
),
)..addListener(() {
if (mounted) {
setState(() => _currentOffset = _overlayOffsetAnimation?.value);
}
});
_messageSizeAnimation = Tween<Size>(
begin: Size(
_originalMessageSize.width,
_originalMessageSize.height,
),
end: _adjustedCenteredMessageSize,
).animate(
CurvedAnimation(
parent: _animationController,
curve: FluffyThemes.animationCurve,
),
);
}
await _animationController.forward(from: 0);
if (mounted) setState(() => _readingAssistanceMode = mode);
}
T _runWithLogging<T>(
@ -215,6 +254,10 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
}
}
final double _inputBarSize = AppConfig.readingAssistanceInputBarHeight +
AppConfig.toolbarButtonsHeight +
20.0;
bool get _showDetails =>
(Matrix.of(context).store.getBool(SettingKeys.displayChatDetailsColumn) ??
false) &&
@ -233,70 +276,12 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
? (FluffyThemes.columnWidth + FluffyThemes.navRailWidth)
: 0;
// message size
RenderBox? get _messageRenderBox => _runWithLogging<RenderBox?>(
() => MatrixState.pAnyState.getRenderBox(
widget.event.eventId,
),
"Error getting message render box",
null,
);
Size get _defaultMessageSize => const Size(FluffyThemes.columnWidth / 2, 100);
Size get _messageSize {
if (_messageRenderBox == null || !_messageRenderBox!.hasSize) {
return _defaultMessageSize;
}
return _runWithLogging(
() => _messageRenderBox?.size,
"Error getting message size",
_defaultMessageSize,
);
}
double get _originalMessageHeight => _messageSize.height;
double? get _centerSpace {
/// Available vertical space not taken up by the header and footer
double? get _verticalSpace {
if (_mediaQuery == null) return null;
return _mediaQuery!.size.height - _headerHeight - _footerHeight;
}
bool get _centeredMessageHasOverflow {
if (_centerSpace == null ||
_centeredMessageSize == null ||
_centeredMessageOffset == null) {
return false;
}
final finalMessageHeight = _centeredMessageSize!.height + _reactionsHeight;
return finalMessageHeight > _centerSpace!;
}
Size? get _adjustedCenteredMessageSize {
if (_centeredMessageHasOverflow) {
return Size(
_centeredMessageSize!.width,
_centerSpace! - (AppConfig.toolbarSpacing * 2),
);
}
return _centeredMessageSize;
}
Offset? get _adjustedCenteredMessageOffset {
if (_centeredMessageHasOverflow) {
return Offset(
_centeredMessageOffset!.dx,
_footerHeight + AppConfig.toolbarSpacing,
);
}
return _centeredMessageOffset;
}
//TODO: figure out where the 16 and 8 come from and use references instead of hard-coded values
static const _messageDefaultLeftMargin = Avatar.defaultSize + 16 + 8;
double get _toolbarMaxWidth {
const double messageMargin = 16.0;
// widget.event.isActivityMessage ? 0 : Avatar.defaultSize + 16 + 8;
@ -318,11 +303,73 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
return maxWidth;
}
// original message size and offset
RenderBox? get _messageRenderBox => _runWithLogging<RenderBox?>(
() => MatrixState.pAnyState.getRenderBox(
widget.event.eventId,
),
"Error getting message render box",
null,
);
Size get _defaultMessageSize => const Size(FluffyThemes.columnWidth / 2, 100);
/// The size of the message in the chat list (as opposed to the expanded size in the center overlay)
Size get _originalMessageSize {
if (_messageRenderBox == null || !_messageRenderBox!.hasSize) {
return _defaultMessageSize;
}
return _runWithLogging(
() => _messageRenderBox?.size,
"Error getting message size",
_defaultMessageSize,
);
}
static const _messageDefaultLeftMargin = Avatar.defaultSize + 16 + 8;
// Centered message size and offset
bool get _centeredMessageHasOverflow {
if (_verticalSpace == null ||
_centeredMessageSize == null ||
_centeredMessageOffset == null) {
return false;
}
final finalMessageHeight = _centeredMessageSize!.height + _reactionsHeight;
return finalMessageHeight > _verticalSpace!;
}
/// Size of the centered overlay message adjusted for overflow
Size? get _adjustedCenteredMessageSize {
if (_centeredMessageHasOverflow) {
return Size(
_centeredMessageSize!.width,
_verticalSpace! - (AppConfig.toolbarSpacing * 2),
);
}
return _centeredMessageSize;
}
Offset? get _adjustedCenteredMessageOffset {
if (_centeredMessageHasOverflow) {
return Offset(
_centeredMessageOffset!.dx,
_footerHeight + AppConfig.toolbarSpacing,
);
}
return _centeredMessageOffset;
}
// message offset
static const Offset _defaultMessageOffset =
Offset(_messageDefaultLeftMargin, 300);
Offset get _messageOffset {
Offset get _originalMessageOffset {
if (_messageRenderBox == null || !_messageRenderBox!.hasSize) {
return _defaultMessageOffset;
}
@ -333,8 +380,50 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
);
}
double get _messageBottomOffset =>
_mediaQuery!.size.height - _messageOffset.dy - _originalMessageHeight;
Offset get _adjustedOriginalMessageOffset {
if (_messageRenderBox == null || !_messageRenderBox!.hasSize) {
return _defaultMessageOffset;
}
final topOffset = _originalMessageOffset.dy;
final bottomOffset = _originalMessageBottomOffset;
final hasHeaderOverflow =
topOffset < (_headerHeight + AppConfig.toolbarSpacing);
final hasFooterOverflow =
bottomOffset < (_footerHeight + AppConfig.toolbarSpacing);
if (!hasHeaderOverflow && !hasFooterOverflow) {
return Offset(
_ownMessage ? _messageRightOffset : _messageLeftOffset,
_originalMessageBottomOffset - _reactionsHeight,
);
}
if (hasHeaderOverflow) {
final difference = topOffset - (_headerHeight + AppConfig.toolbarSpacing);
return Offset(
_ownMessage ? _messageRightOffset : _messageLeftOffset,
_mediaQuery!.size.height -
_originalMessageOffset.dy +
difference -
_originalMessageSize.height,
);
} else {
final difference =
bottomOffset - (_footerHeight + AppConfig.toolbarSpacing);
return Offset(
_ownMessage ? _messageRightOffset : _messageLeftOffset,
_mediaQuery!.size.height -
(_originalMessageOffset.dy + difference) -
_originalMessageSize.height,
);
}
}
double get _originalMessageBottomOffset =>
_mediaQuery!.size.height -
_originalMessageOffset.dy -
_originalMessageSize.height;
double? get _centeredMessageTopOffset {
if (_mediaQuery == null ||
@ -349,7 +438,7 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
}
double get _messageLeftOffset => max(
_messageOffset.dx - _columnWidth - _horizontalPadding,
_originalMessageOffset.dx - _columnWidth - _horizontalPadding,
0,
);
@ -358,8 +447,8 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
return 0;
}
return _mediaQuery!.size.width -
_messageOffset.dx -
_messageSize.width -
_originalMessageOffset.dx -
_originalMessageSize.width -
_horizontalPadding;
}
@ -375,9 +464,7 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
}
double get _footerHeight {
return (_inputBarSize?.height ??
(showToolbarButtons ? AppConfig.toolbarButtonsHeight : 0)) +
(_mediaQuery?.padding.bottom ?? 0);
return _inputBarSize + (_mediaQuery?.padding.bottom ?? 0);
}
// measurement for items in the toolbar
@ -432,13 +519,14 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
child: SizedBox.shrink(),
),
Opacity(
opacity: _finishedAnimation ? 1.0 : 0.0,
opacity:
_readingAssistanceMode == ReadingAssistanceMode.messageMode
? 1.0
: 0.0,
child: OverlayCenterContent(
event: widget.event,
messageHeight: null,
messageWidth: null,
// messageHeight: _adjustedCenteredMessageSize?.height,
// messageWidth: _adjustedCenteredMessageSize?.width,
maxWidth: widget.overlayController.maxWidth,
overlayController: widget.overlayController,
chatController: widget.chatController,
@ -448,11 +536,11 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
hasReactions: _hasReactions,
onChangeMessageSize: _setCenteredMessageSize,
isTransitionAnimation: false,
transitionAnimationFinished: _finishedAnimation,
maxHeight: _mediaQuery!.size.height -
_headerHeight -
_footerHeight -
AppConfig.toolbarSpacing * 2,
readingAssistanceMode: _readingAssistanceMode,
),
),
const Expanded(
@ -466,13 +554,10 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
MeasureRenderBox(
onChange: _setInputBarSize,
child: OverlayFooter(
controller: widget.chatController,
overlayController: widget.overlayController,
showToolbarButtons: showToolbarButtons,
),
OverlayFooter(
controller: widget.chatController,
overlayController: widget.overlayController,
showToolbarButtons: showToolbarButtons,
),
SizedBox(height: _mediaQuery?.padding.bottom ?? 0),
],
@ -486,7 +571,7 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
),
],
),
if (!_finishedAnimation)
if (_readingAssistanceMode != ReadingAssistanceMode.messageMode)
AnimatedBuilder(
animation: _overlayOffsetAnimation ?? _animationController,
builder: (context, child) {
@ -500,11 +585,11 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
_messageRightOffset
: null,
bottom: (_overlayOffsetAnimation?.value)?.dy ??
_messageBottomOffset - _reactionsHeight,
_originalMessageBottomOffset - _reactionsHeight,
child: OverlayCenterContent(
event: widget.event,
messageHeight: _originalMessageHeight,
messageWidth: _messageSize.width,
messageHeight: _originalMessageSize.height,
messageWidth: _originalMessageSize.width,
maxWidth: widget.overlayController.maxWidth,
overlayController: widget.overlayController,
chatController: widget.chatController,
@ -514,11 +599,11 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
hasReactions: _hasReactions,
sizeAnimation: _messageSizeAnimation,
isTransitionAnimation: true,
transitionAnimationFinished: _finishedAnimation,
maxHeight: _mediaQuery!.size.height -
_headerHeight -
_footerHeight -
AppConfig.toolbarSpacing * 2,
readingAssistanceMode: _readingAssistanceMode,
),
);
},
@ -547,7 +632,11 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
),
),
),
if (_centeredMessageTopOffset != null && _tooltipSize != null)
if (_centeredMessageTopOffset != null &&
_tooltipSize != null &&
widget.overlayController.toolbarMode !=
MessageMode.noneSelected &&
widget.overlayController.selectedToken == null)
Positioned(
top: max(
((_headerHeight + _centeredMessageTopOffset!) / 2) -

View file

@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/common/utils/any_state_holder.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
@ -12,6 +11,8 @@ import 'package:fluffychat/pangea/message_token_text/message_token_button.dart';
import 'package:fluffychat/pangea/practice_activities/practice_selection.dart';
import 'package:fluffychat/pangea/practice_activities/practice_selection_repo.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/utils/token_rendering_util.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/utils/url_launcher.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -27,9 +28,9 @@ class MessageTokenText extends StatelessWidget {
final bool Function(PangeaToken)? _isSelected;
final void Function(PangeaToken)? _onClick;
final bool Function(PangeaToken)? _isHighlighted;
final MessageMode? _messageMode;
final MessageOverlayController? _overlayController;
final bool _isTransitionAnimation;
final ReadingAssistanceMode? readingAssistanceMode;
const MessageTokenText({
super.key,
@ -42,11 +43,11 @@ class MessageTokenText extends StatelessWidget {
MessageMode? messageMode,
MessageOverlayController? overlayController,
bool isTransitionAnimation = false,
this.readingAssistanceMode,
}) : _onClick = onClick,
_isSelected = isSelected,
_style = style,
_pangeaMessageEvent = pangeaMessageEvent,
_messageMode = messageMode,
_isHighlighted = isHighlighted,
_overlayController = overlayController,
_isTransitionAnimation = isTransitionAnimation;
@ -82,55 +83,10 @@ class MessageTokenText extends StatelessWidget {
messageAnalyticsEntry: messageAnalyticsEntry,
isSelected: _isSelected,
onClick: callOnClick,
messageMode: _messageMode,
isHighlighted: _isHighlighted,
overlayController: _overlayController,
isTransitionAnimation: _isTransitionAnimation,
);
}
}
class HiddenText extends StatelessWidget {
final String text;
final TextStyle style;
const HiddenText({
super.key,
required this.text,
required this.style,
});
@override
Widget build(BuildContext context) {
final TextPainter textPainter = TextPainter(
text: TextSpan(text: text, style: style),
textDirection: TextDirection.ltr,
)..layout();
final textWidth = textPainter.size.width;
final textHeight = textPainter.size.height;
textPainter.dispose();
return SizedBox(
height: textHeight,
child: Stack(
children: [
Container(
width: textWidth,
height: textHeight,
color: Colors.transparent,
),
Positioned(
bottom: 0,
child: Container(
width: textWidth,
height: 1,
color: style.color,
),
),
],
),
readingAssistanceMode: readingAssistanceMode,
);
}
}
@ -146,12 +102,11 @@ class MessageTextWidget extends StatelessWidget {
final bool? softWrap;
final int? maxLines;
final TextOverflow? overflow;
final MessageMode? messageMode;
final Animation<double>? contentSizeAnimation;
final MessageOverlayController? overlayController;
final bool isTransitionAnimation;
final bool isMessage;
final ReadingAssistanceMode? readingAssistanceMode;
const MessageTextWidget({
super.key,
@ -163,56 +118,23 @@ class MessageTextWidget extends StatelessWidget {
this.softWrap,
this.maxLines,
this.overflow,
this.messageMode,
this.isHighlighted,
this.contentSizeAnimation,
this.overlayController,
this.isTransitionAnimation = false,
this.isMessage = true,
this.readingAssistanceMode,
});
TextStyle style(BuildContext context) => !isMessage
? existingStyle
: overlayController != null && overlayController!.maxWidth > 600
? existingStyle.copyWith(
fontSize: Theme.of(context).textTheme.titleLarge?.fontSize,
)
: existingStyle.copyWith(
fontSize: Theme.of(context).textTheme.bodyLarge?.fontSize,
);
/// for some reason, this isn't the same as tokenTextWidth
double tokenTextWidthForContainer(BuildContext context, PangeaToken token) {
final textPainter = TextPainter(
text: TextSpan(text: token.text.content, style: style(context)),
maxLines: 1,
textDirection: TextDirection.ltr,
)..layout();
final width = textPainter.width;
textPainter.dispose();
return width;
}
Color backgroundColor(BuildContext context, TokenPosition tokenPosition) {
final hideTokenHighlights = messageAnalyticsEntry != null &&
(messageAnalyticsEntry!.hasHiddenWordActivity ||
messageAnalyticsEntry!.hasMessageMeaningActivity);
Color backgroundColor = Colors.transparent;
if (!hideTokenHighlights) {
if (tokenPosition.selected) {
backgroundColor = Theme.of(context).colorScheme.primary;
}
// else if (tokenPosition.isHighlighted) {
// backgroundColor = AppConfig.success.withAlpha(80);
// }
}
return backgroundColor;
}
@override
Widget build(BuildContext context) {
final renderer = TokenRenderingUtil(
pangeaMessageEvent: pangeaMessageEvent,
readingAssistanceMode: readingAssistanceMode,
existingStyle: existingStyle,
overlayController: overlayController,
isTransitionAnimation: isTransitionAnimation,
);
final Characters messageCharacters =
pangeaMessageEvent.messageDisplayText.characters;
@ -226,7 +148,7 @@ class MessageTextWidget extends StatelessWidget {
if (tokenPositions == null) {
return Text(
pangeaMessageEvent.messageDisplayText,
style: style(context),
style: renderer.style(context),
softWrap: softWrap,
maxLines: maxLines,
overflow: overflow,
@ -278,39 +200,45 @@ class MessageTextWidget extends StatelessWidget {
final token = tokenPosition.token!;
final tokenWidth = tokenTextWidthForContainer(context, token);
final tokenWidth = renderer.tokenTextWidthForContainer(
context,
token.text.content,
);
return WidgetSpan(
child: CompositedTransformTarget(
link: overlayController == null || isTransitionAnimation
? LayerLinkAndKey(token.hashCode.toString()).link
: MatrixState.pAnyState
link: renderer.assignTokenKey
? MatrixState.pAnyState
.layerLinkAndKey(token.text.uniqueKey)
.link,
.link
: LayerLinkAndKey(token.hashCode.toString()).link,
child: Column(
key: overlayController == null || isTransitionAnimation
? null
: MatrixState.pAnyState
key: renderer.assignTokenKey
? MatrixState.pAnyState
.layerLinkAndKey(token.text.uniqueKey)
.key,
.key
: null,
children: [
MessageTokenButton(
token: token,
overlayController: overlayController,
textStyle: style(context),
width: tokenWidth,
animate: isTransitionAnimation,
practiceTarget: overlayController
?.toolbarMode.associatedActivityType !=
null
? overlayController?.practiceSelection
?.activities(
overlayController!
.toolbarMode.associatedActivityType!,
)
.firstWhereOrNull((a) => a.tokens.contains(token))
: null,
),
if (renderer.showCenterStyling)
MessageTokenButton(
token: token,
overlayController: overlayController,
textStyle: renderer.style(context),
width: tokenWidth,
animate: isTransitionAnimation,
practiceTarget: overlayController
?.toolbarMode.associatedActivityType !=
null
? overlayController?.practiceSelection
?.activities(
overlayController!
.toolbarMode.associatedActivityType!,
)
.firstWhereOrNull(
(a) => a.tokens.contains(token),
)
: null,
),
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
@ -324,7 +252,13 @@ class MessageTextWidget extends StatelessWidget {
if (start.isNotEmpty)
LinkifySpan(
text: start,
style: style(context),
style: renderer.style(
context,
color: renderer.backgroundColor(
context,
tokenPosition.selected,
),
),
linkStyle: TextStyle(
decoration: TextDecoration.underline,
color: linkColor,
@ -332,39 +266,46 @@ class MessageTextWidget extends StatelessWidget {
onOpen: (url) =>
UrlLauncher(context, url.url).launchUrl(),
),
tokenPosition.hideContent
? WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: GestureDetector(
onTap: onClick != null
? () => onClick?.call(tokenPosition)
: null,
child: HiddenText(
text: middle,
style: style(context),
),
),
)
: LinkifySpan(
text: middle,
// style: style.merge(
// TextStyle(
// backgroundColor: backgroundColor(tokenPosition)
// ),
// ),
style: style(context),
linkStyle: TextStyle(
decoration: TextDecoration.underline,
color: linkColor,
),
onOpen: (url) =>
UrlLauncher(context, url.url)
.launchUrl(),
),
// tokenPosition.hideContent
// ? WidgetSpan(
// alignment: PlaceholderAlignment.middle,
// child: GestureDetector(
// onTap: onClick != null
// ? () => onClick?.call(tokenPosition)
// : null,
// child: HiddenText(
// text: middle,
// style: style(context),
// ),
// ),
// )
// :
LinkifySpan(
text: middle,
style: renderer.style(
context,
color: renderer.backgroundColor(
context,
tokenPosition.selected,
),
),
linkStyle: TextStyle(
decoration: TextDecoration.underline,
color: linkColor,
),
onOpen: (url) =>
UrlLauncher(context, url.url).launchUrl(),
),
if (end.isNotEmpty)
LinkifySpan(
text: end,
style: style(context),
style: renderer.style(
context,
color: renderer.backgroundColor(
context,
tokenPosition.selected,
),
),
linkStyle: TextStyle(
decoration: TextDecoration.underline,
color: linkColor,
@ -377,39 +318,41 @@ class MessageTextWidget extends StatelessWidget {
),
),
),
AnimatedContainer(
duration: const Duration(
milliseconds: AppConfig.overlayAnimationDuration,
),
height:
overlayController != null && !isTransitionAnimation
? 4
: 0,
width: tokenWidth,
child: Container(
color: backgroundColor(context, tokenPosition),
),
),
// AnimatedContainer(
// duration: const Duration(
// milliseconds: AppConfig.overlayAnimationDuration,
// ),
// height: overlayController != null && isTransitionAnimation
// ? 4
// : 0,
// width: tokenWidth,
// child: Container(
// color: backgroundColor(context, tokenPosition),
// ),
// ),
],
),
),
);
} 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(context)),
),
);
}
// 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(context),
// ),
// ),
// );
// }
return LinkifySpan(
text: substring,
style: style(context),
style: renderer.style(context),
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
decoration: TextDecoration.underline,
@ -423,3 +366,48 @@ class MessageTextWidget extends StatelessWidget {
);
}
}
// class HiddenText extends StatelessWidget {
// final String text;
// final TextStyle style;
// const HiddenText({
// super.key,
// required this.text,
// required this.style,
// });
// @override
// Widget build(BuildContext context) {
// final TextPainter textPainter = TextPainter(
// text: TextSpan(text: text, style: style),
// textDirection: TextDirection.ltr,
// )..layout();
// final textWidth = textPainter.size.width;
// final textHeight = textPainter.size.height;
// textPainter.dispose();
// return SizedBox(
// height: textHeight,
// child: Stack(
// children: [
// Container(
// width: textWidth,
// height: textHeight,
// color: Colors.transparent,
// ),
// Positioned(
// bottom: 0,
// child: Container(
// width: textWidth,
// height: 1,
// color: style.color,
// ),
// ),
// ],
// ),
// );
// }
// }

View file

@ -5,6 +5,7 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/events/message_reactions.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/measure_render_box.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/pangea/toolbar/widgets/overlay_message.dart';
@ -29,7 +30,7 @@ class OverlayCenterContent extends StatelessWidget {
final bool hasReactions;
final bool isTransitionAnimation;
final bool transitionAnimationFinished;
final ReadingAssistanceMode? readingAssistanceMode;
const OverlayCenterContent({
required this.event,
@ -46,58 +47,64 @@ class OverlayCenterContent extends StatelessWidget {
this.onChangeMessageSize,
this.sizeAnimation,
this.isTransitionAnimation = false,
this.transitionAnimationFinished = false,
this.readingAssistanceMode,
super.key,
});
@override
Widget build(BuildContext context) {
return Container(
constraints: BoxConstraints(maxWidth: maxWidth),
child: Material(
type: MaterialType.transparency,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: event.senderId == event.room.client.userID
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [
MeasureRenderBox(
onChange: onChangeMessageSize,
child: OverlayMessage(
event,
pangeaMessageEvent: pangeaMessageEvent,
immersionMode: chatController.choreographer.immersionMode,
controller: chatController,
overlayController: overlayController,
nextEvent: nextEvent,
prevEvent: prevEvent,
timeline: chatController.timeline!,
sizeAnimation: sizeAnimation,
// there's a split seconds between when the transition animation starts and
// when the sizeAnimation is set when the original dimensions need to be enforced
messageWidth: (sizeAnimation == null && isTransitionAnimation)
? messageWidth
: null,
messageHeight: (sizeAnimation == null && isTransitionAnimation)
? messageHeight
: null,
maxHeight: maxHeight,
isTransitionAnimation: isTransitionAnimation,
),
),
if (hasReactions)
Padding(
padding: const EdgeInsets.all(4),
child: SizedBox(
height: 20,
child: MessageReactions(
event,
chatController.timeline!,
),
return IgnorePointer(
ignoring: !isTransitionAnimation &&
readingAssistanceMode != ReadingAssistanceMode.messageMode,
child: Container(
constraints: BoxConstraints(maxWidth: maxWidth),
child: Material(
type: MaterialType.transparency,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: event.senderId == event.room.client.userID
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [
MeasureRenderBox(
onChange: onChangeMessageSize,
child: OverlayMessage(
event,
pangeaMessageEvent: pangeaMessageEvent,
immersionMode: chatController.choreographer.immersionMode,
controller: chatController,
overlayController: overlayController,
nextEvent: nextEvent,
prevEvent: prevEvent,
timeline: chatController.timeline!,
sizeAnimation: sizeAnimation,
// there's a split seconds between when the transition animation starts and
// when the sizeAnimation is set when the original dimensions need to be enforced
messageWidth: (sizeAnimation == null && isTransitionAnimation)
? messageWidth
: null,
messageHeight:
(sizeAnimation == null && isTransitionAnimation)
? messageHeight
: null,
maxHeight: maxHeight,
isTransitionAnimation: isTransitionAnimation,
readingAssistanceMode: readingAssistanceMode,
),
),
],
if (hasReactions)
Padding(
padding: const EdgeInsets.all(4),
child: SizedBox(
height: 20,
child: MessageReactions(
event,
chatController.timeline!,
),
),
),
],
),
),
),
);

View file

@ -9,6 +9,7 @@ import 'package:fluffychat/pages/chat/events/message_content.dart';
import 'package:fluffychat/pages/chat/events/reply_content.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -30,6 +31,7 @@ class OverlayMessage extends StatelessWidget {
final double maxHeight;
final bool isTransitionAnimation;
final ReadingAssistanceMode? readingAssistanceMode;
const OverlayMessage(
this.event, {
@ -45,6 +47,7 @@ class OverlayMessage extends StatelessWidget {
this.prevEvent,
this.sizeAnimation,
this.isTransitionAnimation = false,
this.readingAssistanceMode,
super.key,
});
@ -204,6 +207,7 @@ class OverlayMessage extends StatelessWidget {
? theme.colorScheme.onPrimary
: theme.colorScheme.onSurface,
isTransitionAnimation: isTransitionAnimation,
readingAssistanceMode: readingAssistanceMode,
),
if (event.hasAggregatedEvents(
timeline,