Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
253 changes: 134 additions & 119 deletions client/lib/features/chat/presentation/widgets/message_display.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,17 @@ class MessageDisplay extends StatefulWidget {
}

class MessageDisplayState extends State<MessageDisplay> {
bool _isHovered = false;
// Use ValueNotifier so hover-state changes only rebuild the Container's
// color decoration, not the entire widget tree (which would cause
// UserInfoBuilder to reload and briefly show '...' for the display name
// and cause the admin badge to flicker/duplicate in the Wrap layout).
final _isHovered = ValueNotifier<bool>(false);

@override
void dispose() {
_isHovered.dispose();
super.dispose();
}

bool get _canDelete =>
EventPermissionsProvider.watch(context)
Expand All @@ -59,139 +69,144 @@ class MessageDisplayState extends State<MessageDisplay> {
return UserInfoBuilder(
userId: widget.message.creatorId,
builder: (_, isLoading, snapshot) {
return MouseRegion(
onEnter: (hover) => setState(() => _isHovered = true),
onExit: (hover) => setState(() => _isHovered = false),
child: Semantics(
label: context.l10n.chatMessage,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
color: _isHovered
? context.theme.colorScheme.scrim.withScrimOpacity
: null,
child: Row(
// Build the message content once. Passed as the static `child` of
// ValueListenableBuilder so it is not recreated on hover changes.
final content = Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
UserProfileChip(
userId: widget.message.creatorId,
showName: false,
imageHeight: 40,
),
SizedBox(width: 8),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
UserProfileChip(
userId: widget.message.creatorId,
showName: false,
imageHeight: 40,
),
SizedBox(width: 8),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
SelectableText(
semanticsLabel: context.l10n.messageFrom,
snapshot.data?.displayName ?? '...',
style:
context.theme.textTheme.bodyLarge!.copyWith(
color: Theme.of(context).isDark
? context
.theme.colorScheme.onPrimaryContainer
: context.theme.colorScheme.primary,
),
),
SelectableText(
semanticsLabel: context.l10n.messageTime,
' $messageDate, $messageTime$messageTimeZone',
style:
context.theme.textTheme.bodyMedium!.copyWith(
color: Theme.of(context).isDark
? context
.theme.colorScheme.onPrimaryContainer
: context.theme.colorScheme.primary,
),
),
if (isMod)
Container(
color: Theme.of(context).primaryColor,
margin: const EdgeInsets.symmetric(
horizontal: 4,
),
padding: const EdgeInsets.symmetric(
vertical: 3,
horizontal: 4,
),
child: HeightConstrainedText(
isAdmin ? 'ADMIN' : 'MOD',
style: context.theme.textTheme.labelMedium!
.copyWith(
color: context.theme.colorScheme.onPrimary,
),
),
),
if (widget.isSending)
Container(
margin: const EdgeInsets.only(left: 4),
height: 14,
width: 14,
alignment: Alignment.center,
child: CustomLoadingIndicator(),
),
],
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
SelectableText(
semanticsLabel: context.l10n.messageFrom,
snapshot.data?.displayName ?? '...',
style: context.theme.textTheme.bodyLarge!.copyWith(
color: Theme.of(context).isDark
? context.theme.colorScheme.onPrimaryContainer
: context.theme.colorScheme.primary,
),
SizedBox(height: 4),
if (_removed)
HeightConstrainedText(
'This message was removed.',
style: context.theme.textTheme.bodyMedium!.copyWith(
color: Theme.of(context).isDark
? context.theme.colorScheme.onPrimaryContainer
: context.theme.colorScheme.secondary,
fontStyle: FontStyle.italic,
),
)
else
// Note: There are highlighting issues due to the below
// https://github.com/Cretezy/flutter_linkify/issues/59
// https://github.com/Cretezy/flutter_linkify/issues/54
Semantics(
label: context.l10n.chatMessage,
child: SelectableLinkify(
text: widget.message.message ?? '',
style:
context.theme.textTheme.bodyLarge!.copyWith(
color: Theme.of(context).isDark
? context.theme.colorScheme.onPrimary
: context.theme.colorScheme.primary,
),
options: LinkifyOptions(looseUrl: true),
onOpen: (link) async {
await launch(link.url);
},
),
SelectableText(
semanticsLabel: context.l10n.messageTime,
' $messageDate, $messageTime$messageTimeZone',
style: context.theme.textTheme.bodyMedium!.copyWith(
color: Theme.of(context).isDark
? context.theme.colorScheme.onPrimaryContainer
: context.theme.colorScheme.primary,
),
),
if (isMod)
Container(
color: Theme.of(context).primaryColor,
margin: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.symmetric(
vertical: 3,
horizontal: 4,
),
child: HeightConstrainedText(
isAdmin ? 'ADMIN' : 'MOD',
style: context.theme.textTheme.labelMedium!
.copyWith(
color: context.theme.colorScheme.onPrimary,
),
),
],
),
),
if (widget.isSending)
Container(
margin: const EdgeInsets.only(left: 4),
height: 14,
width: 14,
alignment: Alignment.center,
child: CustomLoadingIndicator(),
),
],
),
if (!_removed && _canDelete)
CustomInkWell(
onTap: () => alertOnError(
context,
() => Provider.of<ChatModel>(context, listen: false)
.removeChatMessage(widget.message),
SizedBox(height: 4),
if (_removed)
HeightConstrainedText(
'This message was removed.',
style: context.theme.textTheme.bodyMedium!.copyWith(
color: Theme.of(context).isDark
? context.theme.colorScheme.onPrimaryContainer
: context.theme.colorScheme.secondary,
fontStyle: FontStyle.italic,
),
borderRadius: BorderRadius.circular(15),
child: Padding(
padding: const EdgeInsets.all(6),
child: Icon(
Icons.close,
)
else
// Note: There are highlighting issues due to the below
// https://github.com/Cretezy/flutter_linkify/issues/59
// https://github.com/Cretezy/flutter_linkify/issues/54
Semantics(
label: context.l10n.chatMessage,
child: SelectableLinkify(
text: widget.message.message ?? '',
style: context.theme.textTheme.bodyLarge!.copyWith(
color: Theme.of(context).isDark
? context.theme.colorScheme.surface
? context.theme.colorScheme.onPrimary
: context.theme.colorScheme.primary,
size: 20,
),
options: LinkifyOptions(looseUrl: true),
onOpen: (link) async {
await launch(link.url);
},
),
),
],
),
),
if (!_removed && _canDelete)
CustomInkWell(
onTap: () => alertOnError(
context,
() => Provider.of<ChatModel>(context, listen: false)
.removeChatMessage(widget.message),
),
borderRadius: BorderRadius.circular(15),
child: Padding(
padding: const EdgeInsets.all(6),
child: Icon(
Icons.close,
color: Theme.of(context).isDark
? context.theme.colorScheme.surface
: context.theme.colorScheme.primary,
size: 20,
),
),
),
],
);

return MouseRegion(
onEnter: (_) => _isHovered.value = true,
onExit: (_) => _isHovered.value = false,
child: Semantics(
label: context.l10n.chatMessage,
child: ValueListenableBuilder<bool>(
valueListenable: _isHovered,
// `child` is the pre-built content Row — Flutter reuses it
// unchanged when the ValueNotifier fires, so only the Container
// decoration is re-evaluated on hover.
child: content,
builder: (context, isHovered, child) => Container(
padding:
const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
color: isHovered
? context.theme.colorScheme.scrim.withScrimOpacity
: null,
child: child,
),
),
),
);
},
Expand Down
Loading