frosty/lib/screens/channel/chat/widgets/chat_message.dart
Tommy Chow 9a6892746d
Update to Material 3/You and other changes/fixes (#280)
* Use adaptive refresh indicators

* Add new message count indicator in chat

* Sync pubspec.lock

* Initial migration to material 3

* Restore default bottom sheet style

* Remove FrostyListTile and reset styles

* Use "Clear" button for all search history

* Add `require_trailing_commas` lint rule

* Exclude generated files from linter

* Use ListView for safe area support in bottom sheet

* Add initial dark theme

* Make sizing and units consistent

* Update onbaording spacings & copy

* Migrate to default material 3 buttons

* Add headers to chat details bottom sheet

* Simplify token log in method

* Use bottom sheet for account options

* Remove `AnimateScale` widget

* Tweak search paddings

* Update message spacing divisions

* Upgrade packages

* Update message spacing divisions

* Regenerate splash screens

* Update Inter & add google fonts

* Add initial material 3 themes

* Use adaptive alert dialogs

* Revert to oultined inputs

* Fix tooltip message

* Revert add google_fonts

* Improve themes and various styling

* Convert categories grid to list

* Tweak stream card thumbnail radii

* Tweak settings headers

* Left align floating snackbars

* Tweak search header padding

* Fix profile pics occasionally not rounded

* Use profile pic for settings button if logged in

* Redesign sleep timer

* Use divider theme and reduce thickness

* Use 0 letter spacing for tab bar titles

* Redesign category streams list

* Add border to tooltips

* Increase category card font weight

* Use outlined icon btton for chat user modal

* Reduce profile pic radius in search results

* Fix scrolll to top button safe area

* Add support for reply threads

* Reduce tooltip and snackbar border thickness

* Rebuild reply threads on new messages

* Upgrade packages (iOS)

* Reduce font weight for all chat alerts

* Use tabbars for pageview

* Add dividers everywhere

* Revert "Add dividers everywhere"

This reverts commit 5f0d349fdb.

* Remove fill from inputs

* Add dividers to settings

* Use segmented button for settings list

* Add scrollbar to lists with scrollcontroller

* Redesign badge/emote tooltip

* Remove outline from chat user modal buttons

* Remove divider from reply thread

* Use tap for chatters list and increase font weight

* Constrain emote tooltip width
2023-10-22 18:38:32 -04:00

304 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:frosty/constants.dart';
import 'package:frosty/models/irc.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/reply_thread.dart';
class ChatMessage extends StatelessWidget {
final IRCMessage ircMessage;
final ChatStore chatStore;
final bool isModal;
const ChatMessage({
Key? key,
required this.ircMessage,
required this.chatStore,
this.isModal = false,
}) : super(key: key);
@override
Widget build(BuildContext context) {
void onTapName() {
// 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;
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (context) => ChatUserModal(
chatStore: chatStore,
username: ircMessage.user!,
userId: ircMessage.tags['user-id']!,
displayName: ircMessage.tags['display-name']!,
),
);
}
Future<void> onLongPressMessage() async {
HapticFeedback.lightImpact();
await Clipboard.setData(ClipboardData(text: ircMessage.message ?? ''));
chatStore.updateNotification('Message copied');
}
final defaultTextStyle = DefaultTextStyle.of(context).style;
return Observer(
builder: (context) {
Color? color;
final Widget renderMessage;
switch (ircMessage.command) {
case Command.privateMessage:
case Command.userState:
// If user is being mentioned in the message, highlight it red.
if (ircMessage.mention == true) color = Colors.red.withOpacity(0.2);
if (chatStore.settings.highlightFirstTimeChatter &&
ircMessage.tags['first-msg'] == '1') {
color = Colors.green.withOpacity(0.2);
}
final messageSpan = Text.rich(
TextSpan(
children: ircMessage.generateSpan(
context,
onTapName: onTapName,
style: defaultTextStyle,
assetsStore: chatStore.assetsStore,
emoteScale: chatStore.settings.emoteScale,
badgeScale: chatStore.settings.badgeScale,
useReadableColors: chatStore.settings.useReadableColors,
isLightTheme:
Theme.of(context).brightness == Brightness.light,
launchExternal: chatStore.settings.launchUrlExternal,
timestamp: chatStore.settings.timestampType,
),
),
);
// 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'];
if ((replyUser != null && replyBody != null) ||
(chatStore.settings.highlightFirstTimeChatter &&
ircMessage.tags['first-msg'] == '1')) {
renderMessage = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
replyUser != null && replyBody != null
? Icons.reply_rounded
: Icons.star_rounded,
size: defaultBadgeSize * chatStore.settings.badgeScale,
color: defaultTextStyle.color?.withOpacity(0.5),
textDirection: TextDirection.rtl,
),
const SizedBox(width: 4),
Flexible(
child: replyUser != null && replyBody != null
? GestureDetector(
onTap: isModal
? null
: () => showModalBottomSheet(
context: context,
builder: (context) {
return ReplyThread(
selectedMessage: ircMessage,
chatStore: chatStore,
);
},
),
child: Text(
'Replying to @$replyUser: $replyBody',
maxLines: 1,
style: TextStyle(
overflow: TextOverflow.ellipsis,
color: defaultTextStyle.color
?.withOpacity(0.5),
),
),
)
: Text(
'First time chatting',
style: TextStyle(
fontWeight: FontWeight.w500,
color:
defaultTextStyle.color?.withOpacity(0.5),
),
),
),
],
),
const SizedBox(height: 4),
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.4,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (banDuration == null)
if (ircMessage.command == Command.clearMessage)
const Text(
'Message deleted',
style: TextStyle(fontWeight: FontWeight.w500),
)
else
const Text(
'Permanently banned',
style: TextStyle(fontWeight: FontWeight.w500),
)
else
Text(
'Timed out for $banDuration ${int.parse(banDuration) > 1 ? 'seconds' : 'second'}',
style: const TextStyle(fontWeight: FontWeight.w500),
),
const SizedBox(height: 4),
Text.rich(
TextSpan(
children: ircMessage.generateSpan(
context,
onTapName: onTapName,
style: defaultTextStyle,
assetsStore: chatStore.assetsStore,
emoteScale: chatStore.settings.emoteScale,
badgeScale: chatStore.settings.badgeScale,
showMessage: chatStore.settings.showDeletedMessages,
useReadableColors: chatStore.settings.useReadableColors,
isLightTheme:
Theme.of(context).brightness == Brightness.light,
launchExternal: chatStore.settings.launchUrlExternal,
timestamp: chatStore.settings.timestampType,
),
),
),
],
),
);
break;
case Command.notice:
renderMessage = Text.rich(
TextSpan(text: ircMessage.message),
style: TextStyle(color: defaultTextStyle.color?.withOpacity(0.5)),
);
break;
case Command.userNotice:
if (chatStore.settings.showUserNotices) {
color = Colors.deepPurple.withOpacity(0.2);
renderMessage = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (ircMessage.tags.containsKey('system-msg'))
Text(
ircMessage.tags['system-msg']!,
style: TextStyle(
fontWeight: FontWeight.w500,
color: defaultTextStyle.color?.withOpacity(0.5),
),
),
if (ircMessage.tags['msg-id'] == 'announcement')
Row(
children: [
Icon(
Icons.announcement_outlined,
size:
defaultBadgeSize * chatStore.settings.badgeScale,
),
const SizedBox(width: 4),
const Text(
'Announcement',
style: TextStyle(fontWeight: FontWeight.w500),
),
],
),
const SizedBox(height: 4),
if (ircMessage.message != null)
Text.rich(
TextSpan(
children: ircMessage.generateSpan(
context,
onTapName: onTapName,
style: defaultTextStyle,
assetsStore: chatStore.assetsStore,
emoteScale: chatStore.settings.emoteScale,
badgeScale: chatStore.settings.badgeScale,
useReadableColors:
chatStore.settings.useReadableColors,
isLightTheme:
Theme.of(context).brightness == Brightness.light,
launchExternal: chatStore.settings.launchUrlExternal,
timestamp: chatStore.settings.timestampType,
),
),
),
],
);
} else {
renderMessage = const SizedBox();
}
break;
default:
renderMessage = const SizedBox();
}
final paddedMessage = Padding(
padding: EdgeInsets.symmetric(
vertical: chatStore.settings.messageSpacing / 2,
horizontal: 12,
),
child: renderMessage,
);
// 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 = color == null
? dividedMessage
: ColoredBox(color: color, child: dividedMessage);
return InkWell(
onTap: () {
FocusScope.of(context).unfocus();
if (chatStore.assetsStore.showEmoteMenu) {
chatStore.assetsStore.showEmoteMenu = false;
}
},
onLongPress: onLongPressMessage,
child: coloredMessage,
);
},
);
}
}