Support replying to messages (#300)
* Add message menu with reply * Fix merge * Change endings to LF
This commit is contained in:
parent
2a5a23ef2c
commit
df409378b0
4 changed files with 215 additions and 65 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
''';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue