delthas: 1 Support typing indicators 4 files changed, 87 insertions(+), 1 deletions(-)
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~emersion/goguma-dev/patches/31837/mbox | git am -3Learn more about email & git
The feature is optional & disabled by default. +typing=paused is not supported in this implementation. --- Updated based on your comments. The typing indicator in the subtitle mirrors Whatsapp's behavior and is easier to implement than some text at the bottom of the screen. lib/client_controller.dart | 10 ++++++++- lib/models.dart | 21 +++++++++++++++++++ lib/page/buffer.dart | 42 ++++++++++++++++++++++++++++++++++++++ lib/page/settings.dart | 15 ++++++++++++++ 4 files changed, 87 insertions(+), 1 deletion(-) diff --git a/lib/client_controller.dart b/lib/client_controller.dart index 1497a60..05f1f77 100644 --- a/lib/client_controller.dart +++ b/lib/client_controller.dart @@ -450,7 +450,7 @@ class ClientController { } var buffer = _bufferList.get(msg.source.name, network); - if(buffer != null) { + if (buffer != null) { buffer.realname = realname; _db.storeBuffer(buffer.entry); } @@ -496,6 +496,7 @@ class ClientController { break; case 'PRIVMSG': case 'NOTICE': + case 'TAGMSG': var target = msg.params[0]; if (msg.batchByType('chathistory') != null) { break; @@ -505,6 +506,13 @@ class ClientController { if (!client.isChannel(target) && !client.isMyNick(msg.source.name)) { target = msg.source.name; } + if (msg.cmd == 'TAGMSG') { + var typing = msg.tags['+typing']; + if (typing != null && !client.isMyNick(msg.source.name)) { + _bufferList.get(target, network)?.setTyping(msg.source.name, typing == 'active'); + } + break; + } return _handleChatMessages(target, [msg]); case 'INVITE': var nickname = msg.params[0]; diff --git a/lib/models.dart b/lib/models.dart index f2063ff..7808622 100644 --- a/lib/models.dart +++ b/lib/models.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:collection'; import 'package:flutter/material.dart'; @@ -360,6 +361,7 @@ class BufferModel extends ChangeNotifier { String? _lastDeliveredTime; bool _messageHistoryLoaded = false; List<MessageModel> _messages = []; + Map<String, Timer> _typing = {}; // Kept in sync by BufferPageState bool focused = false; @@ -481,6 +483,25 @@ class BufferModel extends ChangeNotifier { notifyListeners(); return true; } + + List<String> get typing { + var typing = _typing.keys.toList(); + typing.sort(); + return typing; + } + + void setTyping(String member, bool typing) { + _typing[member]?.cancel(); + if (typing) { + _typing[member] = Timer(Duration(seconds: 6), () { + _typing.remove(member); + notifyListeners(); + }); + } else { + _typing.remove(member); + } + notifyListeners(); + } } int _compareMessageModels(MessageModel a, MessageModel b) { diff --git a/lib/page/buffer.dart b/lib/page/buffer.dart index 0e47222..8b620a0 100644 --- a/lib/page/buffer.dart +++ b/lib/page/buffer.dart @@ -78,6 +78,8 @@ class BufferPageState extends State<BufferPage> with WidgetsBindingObserver { bool _showJumpToBottom = false; + DateTime? _ownTyping; + @override void initState() { super.initState(); @@ -111,6 +113,8 @@ class BufferPageState extends State<BufferPage> with WidgetsBindingObserver { var buffer = context.read<BufferModel>(); var client = context.read<Client>(); + _setOwnTyping(false); + var msg = IrcMessage('PRIVMSG', [buffer.name, text]); client.send(msg); @@ -125,6 +129,18 @@ class BufferPageState extends State<BufferPage> with WidgetsBindingObserver { } } + void _sendTypingStatus() { + var buffer = context.read<BufferModel>(); + var client = context.read<Client>(); + + var active = _composerController.text != ''; + var notify = _setOwnTyping(active); + if (notify) { + var msg = IrcMessage('TAGMSG', [buffer.name], tags: {'+typing': active ? 'active' : 'done'}); + client.send(msg); + } + } + void _submitComposer() { if (_composerController.text != '') { _send(_composerController.text); @@ -178,6 +194,21 @@ class BufferPageState extends State<BufferPage> with WidgetsBindingObserver { } } + bool _setOwnTyping(bool active) { + bool notify; + var time = DateTime.now(); + if (!active) { + notify = _ownTyping != null && _ownTyping!.add(Duration(seconds: 6)).isAfter(time); + _ownTyping = null; + } else { + notify = _ownTyping == null || _ownTyping!.add(Duration(seconds: 3)).isBefore(time); + if (notify) { + _ownTyping = time; + } + } + return notify; + } + @override void dispose() { _composerFocusNode.dispose(); @@ -302,6 +333,14 @@ class BufferPageState extends State<BufferPage> with WidgetsBindingObserver { } var messages = buffer.messages; var compact = context.read<SharedPreferences>().getBool('buffer_compact') ?? false; + var showTyping = context.read<SharedPreferences>().getBool('typing_indicator') ?? false; + + if (canSendMessage && showTyping) { + var typingNicks = buffer.typing; + if (typingNicks.isNotEmpty) { + subtitle = typingNicks.join(', ') + ' ${typingNicks.length > 1 ? 'are' : 'is'} typing...'; + } + } Widget? joinBanner; if (isChannel && !buffer.joined && !buffer.joining) { @@ -360,6 +399,9 @@ class BufferPageState extends State<BufferPage> with WidgetsBindingObserver { hintText: 'Write a message...', border: InputBorder.none, ), + onChanged: showTyping ? (value) { + _sendTypingStatus(); + } : null, onSubmitted: (value) { _submitComposer(); }, diff --git a/lib/page/settings.dart b/lib/page/settings.dart index cd90544..cd46f82 100644 --- a/lib/page/settings.dart +++ b/lib/page/settings.dart @@ -24,11 +24,13 @@ class SettingsPage extends StatefulWidget { class _SettingsPageState extends State<SettingsPage> { late bool _compact; + late bool _typing; @override void initState() { super.initState(); _compact = context.read<SharedPreferences>().getBool('buffer_compact') ?? false; + _typing = context.read<SharedPreferences>().getBool('typing_indicator') ?? false; } void _logout() { @@ -120,6 +122,19 @@ class _SettingsPageState extends State<SettingsPage> { }, ), ), + ListTile( + title: Text('Send & display typing indicators'), + leading: Icon(Icons.border_color), + trailing: Switch( + value: _typing, + onChanged: (bool c) { + setState(() { + _typing = c; + context.read<SharedPreferences>().setBool('typing_indicator', c); + }); + }, + ), + ), Divider(), ListTile( title: Text('About'), base-commit: fc4308af156f0642bc7ec7bac4910356e20bdd74 -- 2.17.1
Pushed with a minor edit to disable the feature if the server doesn't support message-tags, thanks!