Support replying to messages (#300)

* Add message menu with reply

* Fix merge

* Change endings to LF
This commit is contained in:
Artiu 2023-11-24 20:36:59 +01:00 • committed by GitHub
parent 2a5a23ef2c
commit df409378b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 215 additions and 65 deletions

View file

@ -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;
}
}

View file

@ -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}
''';
}

View file

@ -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,
),
),
),
],
),
],
),
),

View file

@ -41,7 +41,7 @@ class ChatMessage extends StatelessWidget {
);
}
Future<void> onLongPressMessage() async {
Future<void> 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(