* feat(chat): add bubbles for shared chat participant channels Twitch's native shared chat previously had no way to interact with participant channels the way manually-added chats work. Adds per-participant bubbles alongside manual tabs: tapping one spotlights that channel (fading the rest), hiding one filters its messages from the feed entirely. Defaults to unfaded, resets on rejoin (fresh ChatStore per connect). Manual "merged mode" fading is untouched and unaffected. * refactor(chat): dedup shared-chat tag lookup and extract fade policy - add IRCMessage.sourceChannelId, replacing three copies of the same source-room-id/room-id tag lookup - move the shared-chat spotlight/merged-mode fade decision out of ChatMessage's build into ChatStore.shouldFadeMessage, matching the extract-into-store convention already used for SharedChatBubbleState - factor the tab-bar item's Padding/Center wrapper into a shared _wrapItem helper instead of duplicating it per branch * fix(chat): don't spotlight a hidden shared-chat bubble toggleFocus didn't check hiddenChannelIds, so tapping a hidden bubble's chip added it to the spotlight set — fading every other participant to showcase a channel whose own messages were already filtered out of the feed. * fix(chat): stop shared-chat detection flapping on system messages _isInSharedChatMode was recomputed from every PRIVMSG/NOTICE/USERNOTICE, but only PRIVMSG reliably carries source-room-id for a session's duration. A sub/raid/announcement without the tag would flip the flag off and back on within a single live session. Previously this only risked a redundant asset refetch (already guarded); now the shared chat bubble bar reads the flag directly, so a flap would visibly collapse and reopen it. Scope detection to PRIVMSG only.
653 lines
25 KiB
Dart
653 lines
25 KiB
Dart
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_mobx/flutter_mobx.dart';
|
|
import 'package:frosty/apis/twitch_api.dart';
|
|
import 'package:frosty/constants.dart';
|
|
import 'package:frosty/models/irc.dart';
|
|
import 'package:frosty/models/user.dart';
|
|
import 'package:frosty/screens/channel/chat/stores/chat_store.dart';
|
|
import 'package:frosty/screens/channel/chat/widgets/chat_user_modal.dart';
|
|
import 'package:frosty/screens/channel/chat/widgets/moderation_actions.dart';
|
|
import 'package:frosty/screens/channel/chat/widgets/reply_thread.dart';
|
|
import 'package:frosty/utils/context_extensions.dart';
|
|
import 'package:frosty/utils/modal_bottom_sheet.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
class ChatMessage extends StatelessWidget {
|
|
final IRCMessage ircMessage;
|
|
final ChatStore chatStore;
|
|
final bool isModal;
|
|
final bool forceTimestamp;
|
|
final bool showReplyHeader;
|
|
final bool isInReplyThread;
|
|
|
|
/// Override for merged chat view — provides combined channel profiles.
|
|
final Map<String, UserTwitch>? overrideChannelIdToUserTwitch;
|
|
|
|
/// Override for merged chat view — sets the active/send-target channel ID.
|
|
final String? overrideCurrentChannelId;
|
|
|
|
/// In merged mode, the active tab's ChatStore for notifications and emote
|
|
/// menu. Composer actions (reply/paste) use [onActivateSourceTab] to switch
|
|
/// to the source tab first, then operate on [chatStore] directly.
|
|
final ChatStore? inputChatStore;
|
|
|
|
/// In merged mode, switches the active tab to this message's source channel.
|
|
final VoidCallback? onActivateSourceTab;
|
|
|
|
const ChatMessage({
|
|
super.key,
|
|
required this.ircMessage,
|
|
required this.chatStore,
|
|
this.isModal = false,
|
|
this.forceTimestamp = false,
|
|
this.showReplyHeader = true,
|
|
this.isInReplyThread = false,
|
|
this.overrideChannelIdToUserTwitch,
|
|
this.overrideCurrentChannelId,
|
|
this.inputChatStore,
|
|
this.onActivateSourceTab,
|
|
});
|
|
|
|
void onTapName(BuildContext context) {
|
|
// Ignore if the message is a recent message in the modal bottom sheet.
|
|
if (isModal) return;
|
|
|
|
// Ignore if long-pressing own username.
|
|
if (ircMessage.user == null ||
|
|
ircMessage.user == chatStore.auth.user.details?.login) {
|
|
return;
|
|
}
|
|
|
|
showModalBottomSheetWithProperFocus(
|
|
isScrollControlled: true,
|
|
context: context,
|
|
builder: (context) => ChatUserModal(
|
|
chatStore: chatStore,
|
|
username: ircMessage.user!,
|
|
userId: ircMessage.tags['user-id']!,
|
|
displayName: ircMessage.tags['display-name']!,
|
|
onActivateSourceTab: onActivateSourceTab,
|
|
),
|
|
);
|
|
}
|
|
|
|
void onTapPingedUser(BuildContext context, {required String nickname}) {
|
|
// Ignore if the message is a recent message in the modal bottom sheet.
|
|
if (isModal) return;
|
|
|
|
final twitchApi = context.read<TwitchApi>();
|
|
twitchApi.getUser(userLogin: nickname).then((user) {
|
|
if (context.mounted) {
|
|
showModalBottomSheetWithProperFocus(
|
|
isScrollControlled: true,
|
|
context: context,
|
|
builder: (context) => ChatUserModal(
|
|
chatStore: chatStore,
|
|
username: user.login,
|
|
userId: user.id,
|
|
displayName: user.displayName,
|
|
onActivateSourceTab: onActivateSourceTab,
|
|
),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
/// The store to use for notifications and emote menu in merged mode.
|
|
ChatStore get _effectiveInputStore => inputChatStore ?? chatStore;
|
|
|
|
Future<void> copyMessage() async {
|
|
await Clipboard.setData(ClipboardData(text: ircMessage.message ?? ''));
|
|
|
|
_effectiveInputStore.updateNotification('Message copied');
|
|
}
|
|
|
|
void onLongPressMessage(BuildContext context, TextStyle defaultTextStyle) {
|
|
HapticFeedback.lightImpact();
|
|
|
|
// Allow the full menu for live messages and for ones marked deleted/cleared
|
|
// by a moderation action — those keep their tags (user-id, id), so a
|
|
// moderator can still act on them (e.g. remove a timeout). Everything else
|
|
// (notices, system messages) just copies.
|
|
if (ircMessage.command != Command.privateMessage &&
|
|
ircMessage.command != Command.userState &&
|
|
ircMessage.command != Command.clearChat &&
|
|
ircMessage.command != Command.clearMessage) {
|
|
copyMessage();
|
|
return;
|
|
}
|
|
|
|
showModalBottomSheetWithProperFocus(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
builder: (context) => ListView(
|
|
shrinkWrap: true,
|
|
primary: false,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
child: Text.rich(
|
|
TextSpan(
|
|
children: ircMessage.generateSpan(
|
|
context,
|
|
assetsStore: chatStore.assetsStore,
|
|
emoteScale: chatStore.settings.emoteScale,
|
|
badgeScale: chatStore.settings.badgeScale,
|
|
launchExternal: chatStore.settings.launchUrlExternal,
|
|
timestamp: chatStore.settings.timestampType,
|
|
forceTimestamp: forceTimestamp,
|
|
showHistoricalTimestamps:
|
|
chatStore.settings.showHistoricalTimestamps,
|
|
channelIdToUserTwitch:
|
|
overrideChannelIdToUserTwitch ??
|
|
chatStore.assetsStore.channelIdToUserTwitch,
|
|
currentChannelId:
|
|
overrideCurrentChannelId ?? chatStore.channelId,
|
|
),
|
|
style: defaultTextStyle,
|
|
),
|
|
),
|
|
),
|
|
const Divider(indent: 12, endIndent: 12),
|
|
ListTile(
|
|
onTap: () {
|
|
copyMessage();
|
|
Navigator.pop(context);
|
|
},
|
|
leading: const Icon(Icons.copy),
|
|
title: const Text('Copy message'),
|
|
),
|
|
ListTile(
|
|
onTap: () async {
|
|
await copyMessage();
|
|
|
|
final effectiveChatDelay = chatStore.settings.effectiveChatDelay;
|
|
|
|
final hasChatDelay =
|
|
chatStore.settings.showVideo && effectiveChatDelay > 0;
|
|
|
|
if (hasChatDelay) {
|
|
_effectiveInputStore.updateNotification(
|
|
'Chatting is disabled due to message delay (${effectiveChatDelay.toInt()}s)',
|
|
);
|
|
if (context.mounted) {
|
|
Navigator.pop(context);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// In merged mode, switch to the source tab so the paste
|
|
// targets the correct channel's composer.
|
|
onActivateSourceTab?.call();
|
|
|
|
// Paste the copied message into the text controller
|
|
chatStore.textController.text = ircMessage.message ?? '';
|
|
chatStore.safeRequestFocus();
|
|
if (context.mounted) {
|
|
Navigator.pop(context);
|
|
}
|
|
},
|
|
leading: const Icon(Icons.content_paste),
|
|
title: const Text('Copy message and paste'),
|
|
),
|
|
ListTile(
|
|
onTap: () {
|
|
// In merged mode, switch to the source tab so the reply
|
|
// targets the correct channel (reply-parent-msg-id is
|
|
// channel-scoped).
|
|
onActivateSourceTab?.call();
|
|
|
|
chatStore.replyingToMessage = ircMessage;
|
|
chatStore.safeRequestFocus();
|
|
Navigator.pop(context);
|
|
},
|
|
leading: const Icon(Icons.reply),
|
|
title: const Text('Reply to message'),
|
|
),
|
|
if (ircMessage.tags['user-id'] != null)
|
|
ModerationActions(
|
|
chatStore: chatStore,
|
|
targetUserId: ircMessage.tags['user-id']!,
|
|
targetName:
|
|
ircMessage.tags['display-name'] ?? ircMessage.user ?? '',
|
|
messageId: ircMessage.tags['id'],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final defaultTextStyle = DefaultTextStyle.of(context).style;
|
|
final messageHeaderIconSize =
|
|
defaultBadgeSize * chatStore.settings.badgeScale;
|
|
final messageHeaderTextColor = defaultTextStyle.color?.withValues(
|
|
alpha: 0.5,
|
|
);
|
|
const messageHeaderFontWeight = FontWeight.w600;
|
|
|
|
return Observer(
|
|
builder: (context) {
|
|
Color? highlightColor;
|
|
final Widget renderMessage;
|
|
|
|
switch (ircMessage.command) {
|
|
case Command.privateMessage:
|
|
case Command.userState:
|
|
final shouldHighlightFirstMessage =
|
|
chatStore.settings.highlightFirstTimeChatter &&
|
|
ircMessage.tags['first-msg'] == '1';
|
|
final shouldHighlightMessage =
|
|
chatStore.settings.showUserNotices &&
|
|
ircMessage.tags['msg-id'] == 'highlighted-message';
|
|
|
|
final messageSpan = Text.rich(
|
|
TextSpan(
|
|
children: ircMessage.generateSpan(
|
|
context,
|
|
onTapName: () => onTapName(context),
|
|
onTapPingedUser: (nickname) =>
|
|
onTapPingedUser(context, nickname: nickname),
|
|
style: defaultTextStyle,
|
|
assetsStore: chatStore.assetsStore,
|
|
emoteScale: chatStore.settings.emoteScale,
|
|
badgeScale: chatStore.settings.badgeScale,
|
|
launchExternal: chatStore.settings.launchUrlExternal,
|
|
timestamp: chatStore.settings.timestampType,
|
|
forceTimestamp: forceTimestamp,
|
|
showHistoricalTimestamps:
|
|
chatStore.settings.showHistoricalTimestamps,
|
|
channelIdToUserTwitch:
|
|
overrideChannelIdToUserTwitch ??
|
|
chatStore.assetsStore.channelIdToUserTwitch,
|
|
currentChannelId:
|
|
overrideCurrentChannelId ?? chatStore.channelId,
|
|
),
|
|
),
|
|
);
|
|
|
|
// Check if the message is replying to another message.
|
|
final replyUser = ircMessage.tags['reply-parent-display-name'];
|
|
final replyBody = ircMessage.tags['reply-parent-msg-body'];
|
|
|
|
Widget? messageHeaderIcon;
|
|
Widget? messageHeader;
|
|
if (replyUser != null && replyBody != null && showReplyHeader) {
|
|
messageHeaderIcon = Icon(
|
|
Icons.chat_rounded,
|
|
size: messageHeaderIconSize,
|
|
color: messageHeaderTextColor,
|
|
);
|
|
messageHeader = GestureDetector(
|
|
onTap: isModal
|
|
? null
|
|
: () => showModalBottomSheetWithProperFocus(
|
|
context: context,
|
|
builder: (context) {
|
|
return ReplyThread(
|
|
selectedMessage: ircMessage,
|
|
chatStore: chatStore,
|
|
);
|
|
},
|
|
),
|
|
child: Text.rich(
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
TextSpan(
|
|
children: [
|
|
TextSpan(
|
|
text: replyUser,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
color: messageHeaderTextColor,
|
|
),
|
|
),
|
|
TextSpan(
|
|
text: ': $replyBody',
|
|
style: TextStyle(color: messageHeaderTextColor),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
} else if (shouldHighlightFirstMessage) {
|
|
highlightColor = context.frostyColors.highlightedMessage;
|
|
messageHeaderIcon = Icon(
|
|
Icons.auto_awesome_rounded,
|
|
size: messageHeaderIconSize,
|
|
color: messageHeaderTextColor,
|
|
);
|
|
messageHeader = Text(
|
|
'First message',
|
|
style: TextStyle(
|
|
fontWeight: messageHeaderFontWeight,
|
|
color: messageHeaderTextColor,
|
|
),
|
|
);
|
|
} else if (shouldHighlightMessage) {
|
|
highlightColor = context.frostyColors.highlightedMessage;
|
|
messageHeader = Text(
|
|
'Highlighted message',
|
|
style: TextStyle(
|
|
fontWeight: messageHeaderFontWeight,
|
|
color: messageHeaderTextColor,
|
|
),
|
|
);
|
|
}
|
|
|
|
// If user is being mentioned in the message, highlight it red.
|
|
if (ircMessage.mention == true) {
|
|
highlightColor = context.colorScheme.error;
|
|
}
|
|
|
|
if (messageHeader != null) {
|
|
renderMessage = Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
spacing: 4,
|
|
children: [
|
|
if (messageHeaderIcon != null)
|
|
Row(
|
|
spacing: 4,
|
|
children: [
|
|
messageHeaderIcon,
|
|
Flexible(child: messageHeader),
|
|
],
|
|
)
|
|
else
|
|
messageHeader,
|
|
messageSpan,
|
|
],
|
|
);
|
|
} else {
|
|
renderMessage = messageSpan;
|
|
}
|
|
|
|
break;
|
|
case Command.clearChat:
|
|
case Command.clearMessage:
|
|
// Render timeouts and bans
|
|
final banDuration = ircMessage.tags['ban-duration'];
|
|
|
|
renderMessage = Opacity(
|
|
opacity: 0.5,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
spacing: 4,
|
|
children: [
|
|
if (banDuration == null)
|
|
if (ircMessage.command == Command.clearMessage)
|
|
const Text(
|
|
'Message deleted',
|
|
style: TextStyle(fontWeight: messageHeaderFontWeight),
|
|
)
|
|
else
|
|
const Text(
|
|
'Permanently banned',
|
|
style: TextStyle(fontWeight: messageHeaderFontWeight),
|
|
)
|
|
else
|
|
Text(
|
|
'Timed out for $banDuration ${int.parse(banDuration) > 1 ? 'seconds' : 'second'}',
|
|
style: const TextStyle(
|
|
fontWeight: messageHeaderFontWeight,
|
|
),
|
|
),
|
|
Text.rich(
|
|
TextSpan(
|
|
children: ircMessage.generateSpan(
|
|
context,
|
|
onTapName: () => onTapName(context),
|
|
style: defaultTextStyle,
|
|
assetsStore: chatStore.assetsStore,
|
|
emoteScale: chatStore.settings.emoteScale,
|
|
badgeScale: chatStore.settings.badgeScale,
|
|
showMessage:
|
|
chatStore.settings.showDeletedMessages ||
|
|
chatStore.revealedMessageIds.contains(
|
|
ircMessage.tags['id'],
|
|
),
|
|
onTapDeletedMessage: () {
|
|
if (ircMessage.tags['id'] != null) {
|
|
chatStore.revealMessage(ircMessage.tags['id']!);
|
|
}
|
|
},
|
|
launchExternal: chatStore.settings.launchUrlExternal,
|
|
timestamp: chatStore.settings.timestampType,
|
|
forceTimestamp: forceTimestamp,
|
|
showHistoricalTimestamps:
|
|
chatStore.settings.showHistoricalTimestamps,
|
|
channelIdToUserTwitch:
|
|
overrideChannelIdToUserTwitch ??
|
|
chatStore.assetsStore.channelIdToUserTwitch,
|
|
currentChannelId:
|
|
overrideCurrentChannelId ?? chatStore.channelId,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
break;
|
|
case Command.notice:
|
|
// Use tabular figures so countdown numbers don't shift horizontally
|
|
final noticeStyle = TextStyle(
|
|
color: messageHeaderTextColor,
|
|
fontFeatures: const [FontFeature.tabularFigures()],
|
|
);
|
|
if (ircMessage.actionCallback != null &&
|
|
ircMessage.actionLabel != null) {
|
|
renderMessage = Text.rich(
|
|
TextSpan(
|
|
children: [
|
|
TextSpan(
|
|
text: ircMessage.message ?? '',
|
|
style: noticeStyle,
|
|
),
|
|
TextSpan(text: ' '),
|
|
TextSpan(
|
|
text: ircMessage.actionLabel!,
|
|
style: TextStyle(
|
|
color: Theme.of(context).colorScheme.primary,
|
|
fontWeight: FontWeight.w600,
|
|
decoration: TextDecoration.underline,
|
|
decorationColor: Theme.of(context).colorScheme.primary,
|
|
),
|
|
recognizer: TapGestureRecognizer()
|
|
..onTap = ircMessage.actionCallback,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
} else {
|
|
renderMessage = Text.rich(
|
|
TextSpan(text: ircMessage.message),
|
|
style: noticeStyle,
|
|
);
|
|
}
|
|
break;
|
|
case Command.userNotice:
|
|
if (chatStore.settings.showUserNotices) {
|
|
highlightColor = context.frostyColors.highlightedMessage;
|
|
Widget? messageHeaderIcon;
|
|
Widget? messageHeader;
|
|
|
|
if (ircMessage.tags.containsKey('system-msg')) {
|
|
final messageId = ircMessage.tags['msg-id'];
|
|
final isGift = messageId?.contains('gift') == true;
|
|
final isSub = messageId?.contains('sub') == true;
|
|
final isRaid = messageId?.contains('raid') == true;
|
|
final isPrime =
|
|
ircMessage.tags['msg-param-sub-plan'] == 'Prime';
|
|
|
|
IconData? icon;
|
|
if (isGift) {
|
|
icon = Icons.card_giftcard_rounded;
|
|
} else if (isSub) {
|
|
icon = Icons.star_rounded;
|
|
} else if (isRaid) {
|
|
icon = Icons.people_rounded;
|
|
} else if (isPrime) {
|
|
icon = Icons.workspace_premium_rounded;
|
|
}
|
|
|
|
if (icon != null) {
|
|
messageHeaderIcon = Icon(
|
|
icon,
|
|
size: messageHeaderIconSize,
|
|
color: messageHeaderTextColor,
|
|
);
|
|
}
|
|
|
|
messageHeader = Text(
|
|
ircMessage.tags['system-msg']!,
|
|
style: TextStyle(
|
|
fontWeight: messageHeaderFontWeight,
|
|
color: messageHeaderTextColor,
|
|
),
|
|
);
|
|
} else if (ircMessage.tags['msg-id'] == 'announcement') {
|
|
messageHeaderIcon = Icon(
|
|
Icons.campaign_rounded,
|
|
size: messageHeaderIconSize,
|
|
color: messageHeaderTextColor,
|
|
);
|
|
messageHeader = Text(
|
|
'Announcement',
|
|
style: TextStyle(
|
|
fontWeight: messageHeaderFontWeight,
|
|
color: messageHeaderTextColor,
|
|
),
|
|
);
|
|
}
|
|
|
|
renderMessage = Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
spacing: 4,
|
|
children: [
|
|
if (messageHeader != null)
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
spacing: 4,
|
|
children: [
|
|
if (messageHeaderIcon != null) ...[messageHeaderIcon],
|
|
Expanded(child: messageHeader),
|
|
],
|
|
),
|
|
if (ircMessage.message != null) ...[
|
|
Text.rich(
|
|
TextSpan(
|
|
children: ircMessage.generateSpan(
|
|
context,
|
|
onTapName: () => onTapName(context),
|
|
style: defaultTextStyle,
|
|
assetsStore: chatStore.assetsStore,
|
|
emoteScale: chatStore.settings.emoteScale,
|
|
badgeScale: chatStore.settings.badgeScale,
|
|
launchExternal: chatStore.settings.launchUrlExternal,
|
|
timestamp: chatStore.settings.timestampType,
|
|
forceTimestamp: forceTimestamp,
|
|
showHistoricalTimestamps:
|
|
chatStore.settings.showHistoricalTimestamps,
|
|
channelIdToUserTwitch:
|
|
overrideChannelIdToUserTwitch ??
|
|
chatStore.assetsStore.channelIdToUserTwitch,
|
|
currentChannelId:
|
|
overrideCurrentChannelId ?? chatStore.channelId,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
);
|
|
} else {
|
|
renderMessage = const SizedBox();
|
|
}
|
|
break;
|
|
default:
|
|
renderMessage = const SizedBox();
|
|
}
|
|
|
|
// Check if this is a reply message in reply thread context for indentation
|
|
final isReplyInThread =
|
|
isInReplyThread &&
|
|
ircMessage.tags['reply-parent-display-name'] != null &&
|
|
ircMessage.tags['reply-parent-msg-body'] != null;
|
|
|
|
// Add reply icon for messages in reply thread
|
|
final messageWithIcon = isReplyInThread
|
|
? Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(right: 8.0, top: 2.0),
|
|
child: Icon(
|
|
Icons.subdirectory_arrow_right_rounded,
|
|
size: 16,
|
|
color: messageHeaderTextColor,
|
|
),
|
|
),
|
|
Expanded(child: renderMessage),
|
|
],
|
|
)
|
|
: renderMessage;
|
|
|
|
final paddedMessage = Padding(
|
|
padding: EdgeInsets.only(
|
|
top: chatStore.settings.messageSpacing / 2,
|
|
bottom: chatStore.settings.messageSpacing / 2,
|
|
left: isReplyInThread ? 16 : (highlightColor == null ? 12 : 0),
|
|
right: highlightColor == null ? 12 : 0,
|
|
),
|
|
child: messageWithIcon,
|
|
);
|
|
|
|
// Add a divider above the message if dividers are enabled.
|
|
final dividedMessage = chatStore.settings.showChatMessageDividers
|
|
? Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [paddedMessage, const Divider()],
|
|
)
|
|
: paddedMessage;
|
|
|
|
// Color the message if the color has been set.
|
|
final coloredMessage = highlightColor == null
|
|
? dividedMessage
|
|
: Container(
|
|
padding: const EdgeInsets.only(left: 8, right: 12),
|
|
decoration: BoxDecoration(
|
|
color: highlightColor.withValues(alpha: 0.1),
|
|
border: Border(
|
|
left: BorderSide(color: highlightColor, width: 4),
|
|
),
|
|
),
|
|
child: dividedMessage,
|
|
);
|
|
|
|
final shouldFade = chatStore.shouldFadeMessage(
|
|
sourceChannelId: ircMessage.sourceChannelId,
|
|
overrideCurrentChannelId: overrideCurrentChannelId,
|
|
);
|
|
|
|
final fadedMessage = shouldFade
|
|
? Opacity(opacity: 0.55, child: coloredMessage)
|
|
: coloredMessage;
|
|
|
|
final finalMessage = InkWell(
|
|
onTap: () {
|
|
FocusScope.of(context).unfocus();
|
|
if (_effectiveInputStore.assetsStore.showEmoteMenu) {
|
|
_effectiveInputStore.assetsStore.showEmoteMenu = false;
|
|
}
|
|
},
|
|
onLongPress: () => onLongPressMessage(context, defaultTextStyle),
|
|
child: fadedMessage,
|
|
);
|
|
|
|
return finalMessage;
|
|
},
|
|
);
|
|
}
|
|
}
|