diff --git a/lib/screens/channel/chat/stores/chat_store.dart b/lib/screens/channel/chat/stores/chat_store.dart index 2158a3d3..5833b953 100644 --- a/lib/screens/channel/chat/stores/chat_store.dart +++ b/lib/screens/channel/chat/stores/chat_store.dart @@ -138,6 +138,9 @@ abstract class ChatStoreBase with Store { @observable var expandChat = false; + @observable + IRCMessage? replyingToMessage; + ChatStoreBase({ required this.auth, required this.chatDetailsStore, @@ -274,7 +277,8 @@ abstract class ChatStoreBase with Store { case Command.userState: _userState = _userState.fromIRCMessage(parsedIRCMessage); - if (toSend != null) { + if (toSend != null && parsedIRCMessage.tags['id'] != null) { + toSend!.tags['id'] = parsedIRCMessage.tags['id']!; textController.clear(); messageBuffer.add(toSend!); toSend = null; @@ -443,7 +447,9 @@ abstract class ChatStoreBase with Store { ); } else { // Send the message to the IRC chat room. - _channel?.sink.add('PRIVMSG #$channelName :$message'); + _channel?.sink.add( + '${replyingToMessage != null ? '@reply-parent-msg-id=${replyingToMessage!.tags['id']} ' : ''}PRIVMSG #$channelName :$message', + ); // Obtain the logged-in user's appearance in chat with USERSTATE and create the full message to render. var userStateString = _userState.raw; @@ -452,7 +458,8 @@ abstract class ChatStoreBase with Store { userStateString += ' :\x01ACTION ${message.replaceRange(0, 3, '').trim()}\x01'; } else { - userStateString += ' :${message.trim()}'; + userStateString += + ' :${replyingToMessage?.tags['display-name'] != null ? '@${replyingToMessage!.tags['display-name']} ' : ''}${message.trim()}'; } final userChatMessage = IRCMessage.fromString(userStateString); @@ -461,6 +468,17 @@ abstract class ChatStoreBase with Store { userChatMessage.tags['user-id'] = auth.user.details!.id; } + if (replyingToMessage != null && + replyingToMessage!.tags['id'] != null) { + userChatMessage.tags['reply-parent-msg-id'] = + replyingToMessage!.tags['id']!; + userChatMessage.tags['reply-parent-display-name'] = + replyingToMessage!.tags['display-name']!; + userChatMessage.tags['reply-parent-msg-body'] = + replyingToMessage!.message!; + } + + replyingToMessage = null; toSend = userChatMessage; } } diff --git a/lib/screens/channel/chat/stores/chat_store.g.dart b/lib/screens/channel/chat/stores/chat_store.g.dart index 57ef152f..12d8ea9a 100644 --- a/lib/screens/channel/chat/stores/chat_store.g.dart +++ b/lib/screens/channel/chat/stores/chat_store.g.dart @@ -195,6 +195,22 @@ mixin _$ChatStore on ChatStoreBase, Store { }); } + late final _$replyingToMessageAtom = + Atom(name: 'ChatStoreBase.replyingToMessage', context: context); + + @override + IRCMessage? get replyingToMessage { + _$replyingToMessageAtom.reportRead(); + return super.replyingToMessage; + } + + @override + set replyingToMessage(IRCMessage? value) { + _$replyingToMessageAtom.reportWrite(value, super.replyingToMessage, () { + super.replyingToMessage = value; + }); + } + late final _$getAssetsAsyncAction = AsyncAction('ChatStoreBase.getAssets', context: context); @@ -312,6 +328,7 @@ mixin _$ChatStore on ChatStoreBase, Store { return ''' timeRemaining: ${timeRemaining}, expandChat: ${expandChat}, +replyingToMessage: ${replyingToMessage}, renderMessages: ${renderMessages} '''; } diff --git a/lib/screens/channel/chat/widgets/chat_bottom_bar.dart b/lib/screens/channel/chat/widgets/chat_bottom_bar.dart index 51c90991..b34eae98 100644 --- a/lib/screens/channel/chat/widgets/chat_bottom_bar.dart +++ b/lib/screens/channel/chat/widgets/chat_bottom_bar.dart @@ -130,72 +130,123 @@ class ChatBottomBar extends StatelessWidget { ], Padding( padding: const EdgeInsets.fromLTRB(12, 12, 0, 12), - child: Row( + child: Column( children: [ - if (!chatStore.expandChat && - chatStore.settings.chatWidth < 0.3 && - chatStore.settings.showVideo && - MediaQuery.of(context).orientation == - Orientation.landscape) - IconButton( - tooltip: 'Enter a message', - onPressed: () { - chatStore.expandChat = true; - chatStore.textFieldFocusNode.requestFocus(); - }, - icon: const Icon(Icons.edit), - ) - else - Expanded( - child: TextField( - textInputAction: TextInputAction.send, - focusNode: chatStore.textFieldFocusNode, - minLines: 1, - maxLines: 3, - enabled: chatStore.auth.isLoggedIn ? true : false, - decoration: InputDecoration( - prefixIcon: chatStore.settings.emoteMenuButtonOnLeft - ? emoteMenuButton - : null, - suffixIcon: chatStore.settings.emoteMenuButtonOnLeft - ? null - : emoteMenuButton, - hintMaxLines: 1, - hintText: chatStore.auth.isLoggedIn - ? 'Send a message ${chatStore.settings.chatDelay == 0 ? '' : '(${chatStore.settings.chatDelay.toInt()}s delay)'}' - : 'Log in to chat', - ), - controller: chatStore.textController, - onSubmitted: chatStore.sendMessage, + if (chatStore.replyingToMessage != null) + Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.reply), + Expanded( + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 12), + child: Text.rich( + TextSpan( + children: + chatStore.replyingToMessage!.generateSpan( + context, + 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, + style: null, + onTapName: null, + ), + ), + maxLines: 5, + overflow: TextOverflow.ellipsis, + ), + ), + ), + IconButton( + onPressed: () { + chatStore.replyingToMessage = null; + }, + icon: const Icon(Icons.close), + ), + ], ), ), - if (chatStore.showSendButton && - (chatStore.settings.chatWidth >= 0.3 || - chatStore.expandChat || + Row( + children: [ + if (!chatStore.expandChat && + chatStore.settings.chatWidth < 0.3 && + chatStore.settings.showVideo && MediaQuery.of(context).orientation == - Orientation.portrait)) - IconButton( - tooltip: 'Send', - icon: const Icon(Icons.send_rounded), - onPressed: chatStore.auth.isLoggedIn - ? () => chatStore - .sendMessage(chatStore.textController.text) - : null, - ) - else - IconButton( - icon: Icon(Icons.adaptive.more_rounded), - tooltip: 'More', - onPressed: () => showModalBottomSheet( - isScrollControlled: true, - context: context, - builder: (_) => ChatDetails( - chatDetailsStore: chatStore.chatDetailsStore, - chatStore: chatStore, - userLogin: chatStore.channelName, + Orientation.landscape) + IconButton( + tooltip: 'Enter a message', + onPressed: () { + chatStore.expandChat = true; + chatStore.textFieldFocusNode.requestFocus(); + }, + icon: const Icon(Icons.edit), + ) + else + Expanded( + child: TextField( + textInputAction: TextInputAction.send, + focusNode: chatStore.textFieldFocusNode, + minLines: 1, + maxLines: 3, + enabled: chatStore.auth.isLoggedIn ? true : false, + decoration: InputDecoration( + prefixIcon: + chatStore.settings.emoteMenuButtonOnLeft + ? emoteMenuButton + : null, + suffixIcon: + chatStore.settings.emoteMenuButtonOnLeft + ? null + : emoteMenuButton, + hintMaxLines: 1, + hintText: chatStore.auth.isLoggedIn + ? 'Send a message ${chatStore.settings.chatDelay == 0 ? '' : '(${chatStore.settings.chatDelay.toInt()}s delay)'}' + : 'Log in to chat', + ), + controller: chatStore.textController, + onSubmitted: chatStore.sendMessage, + ), ), - ), - ), + if (chatStore.showSendButton && + (chatStore.settings.chatWidth >= 0.3 || + chatStore.expandChat || + MediaQuery.of(context).orientation == + Orientation.portrait)) + IconButton( + tooltip: 'Send', + icon: const Icon(Icons.send_rounded), + onPressed: chatStore.auth.isLoggedIn + ? () => chatStore + .sendMessage(chatStore.textController.text) + : null, + ) + else + IconButton( + icon: Icon(Icons.adaptive.more_rounded), + tooltip: 'More', + onPressed: () => showModalBottomSheet( + isScrollControlled: true, + context: context, + builder: (_) => ChatDetails( + chatDetailsStore: chatStore.chatDetailsStore, + chatStore: chatStore, + userLogin: chatStore.channelName, + ), + ), + ), + ], + ), ], ), ), diff --git a/lib/screens/channel/chat/widgets/chat_message.dart b/lib/screens/channel/chat/widgets/chat_message.dart index 483fe387..fd2b320b 100644 --- a/lib/screens/channel/chat/widgets/chat_message.dart +++ b/lib/screens/channel/chat/widgets/chat_message.dart @@ -41,7 +41,7 @@ class ChatMessage extends StatelessWidget { ); } - Future onLongPressMessage() async { + Future copyMessage() async { HapticFeedback.lightImpact(); await Clipboard.setData(ClipboardData(text: ircMessage.message ?? '')); @@ -49,6 +49,70 @@ class ChatMessage extends StatelessWidget { chatStore.updateNotification('Message copied'); } + void onLongPressMessage() { + if (ircMessage.command != Command.privateMessage && + ircMessage.command != Command.userState) { + copyMessage(); + return; + } + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => ListView( + shrinkWrap: true, + primary: false, + children: [ + ListTile( + title: Text.rich( + TextSpan( + children: ircMessage.generateSpan( + context, + 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, + style: null, + onTapName: null, + ), + ), + ), + ), + ListTile( + onTap: () { + copyMessage(); + Navigator.pop(context); + }, + leading: const Icon(Icons.copy), + title: const Text( + 'Copy', + style: TextStyle( + fontWeight: FontWeight.w600, + ), + ), + ), + ListTile( + onTap: () { + chatStore.replyingToMessage = ircMessage; + chatStore.textFieldFocusNode.requestFocus(); + Navigator.pop(context); + }, + leading: const Icon(Icons.reply), + title: const Text( + 'Reply', + style: TextStyle( + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ); + } + final defaultTextStyle = DefaultTextStyle.of(context).style; return Observer(