From 8f65802ab19d8c5d9e5c257fb958bb18adbcc20c Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Sat, 11 Jul 2026 12:02:47 +0930 Subject: [PATCH] Show room typing activity in room panels --- commet/lib/ui/atoms/room_panel.dart | 16 +++ commet/lib/ui/atoms/room_panel_view.dart | 16 ++- .../molecules/typing_indicators_widget.dart | 126 ++++++++++-------- 3 files changed, 101 insertions(+), 57 deletions(-) diff --git a/commet/lib/ui/atoms/room_panel.dart b/commet/lib/ui/atoms/room_panel.dart index dd443cc76..bbf36ab14 100644 --- a/commet/lib/ui/atoms/room_panel.dart +++ b/commet/lib/ui/atoms/room_panel.dart @@ -1,7 +1,9 @@ import 'dart:async'; import 'package:commet/client/components/direct_messages/direct_message_component.dart'; +import 'package:commet/client/components/typing_indicators/typing_indicator_component.dart'; import 'package:commet/client/components/user_presence/user_presence_component.dart'; +import 'package:commet/client/member.dart'; import 'package:commet/client/room.dart'; import 'package:commet/ui/atoms/adaptive_context_menu.dart'; import 'package:commet/ui/atoms/room_panel_view.dart'; @@ -26,6 +28,7 @@ class _RoomPanelState extends State { late List subs; String? directMessagePartner; UserPresence? presence = null; + List typingUsers = []; @override void initState() { @@ -35,6 +38,18 @@ class _RoomPanelState extends State { var dm = widget.room.client.getComponent(); directMessagePartner = dm?.getDirectMessagePartnerId(widget.room); + var typing = widget.room.getComponent(); + + if (typing != null) { + subs.add(typing.onTypingUsersUpdated.listen((_) { + setState(() { + typingUsers = typing.typingUsers; + }); + })); + + typingUsers = typing.typingUsers; + } + if (directMessagePartner != null) { final presenceComponent = widget.room.client.getComponent(); @@ -108,6 +123,7 @@ class _RoomPanelState extends State { : null, body: widget.room.lastMessage?.plainTextBody, notificationCount: widget.room.notificationCount, + typingMembers: typingUsers, highlightNotificationCount: widget.room.displayHighlightedNotificationCount, ), diff --git a/commet/lib/ui/atoms/room_panel_view.dart b/commet/lib/ui/atoms/room_panel_view.dart index 048edb7f1..ea61c00c3 100644 --- a/commet/lib/ui/atoms/room_panel_view.dart +++ b/commet/lib/ui/atoms/room_panel_view.dart @@ -1,7 +1,9 @@ import 'package:commet/client/components/user_presence/user_presence_component.dart'; +import 'package:commet/client/member.dart'; import 'package:commet/ui/atoms/dot_indicator.dart'; import 'package:commet/ui/atoms/notification_badge.dart'; import 'package:commet/ui/atoms/shimmer_loading.dart'; +import 'package:commet/ui/molecules/typing_indicators_widget.dart'; import 'package:commet/ui/molecules/user_panel.dart'; import 'package:flutter/material.dart'; import 'package:tiamat/tiamat.dart'; @@ -30,6 +32,7 @@ class RoomPanelView extends StatefulWidget { this.showUserAvatar = false, this.notificationCount = 0, this.highlightNotificationCount = 0, + this.typingMembers, super.key}); final ImageProvider? avatar; final ImageProvider? userAvatar; @@ -46,6 +49,7 @@ class RoomPanelView extends StatefulWidget { final String? secondaryButtonLabel; final Future Function()? onSecondaryButtonPressed; final bool showUserAvatar; + final List? typingMembers; final bool loading; final String? directMessagePartner; final double random; @@ -182,8 +186,16 @@ class _RoomPanelViewState extends State { maxLines: 1, overflow: TextOverflow.ellipsis, ), - if (widget.body != null) - Flexible(child: recentEvent()) + if (widget.typingMembers?.isNotEmpty == + true) + SizedBox( + height: 16, + child: TypingIndicatorAnimation()), + if (widget.typingMembers?.isNotEmpty != + true && + widget.body != null) + SizedBox( + height: 16, child: recentEvent()), ], ), ), diff --git a/commet/lib/ui/molecules/typing_indicators_widget.dart b/commet/lib/ui/molecules/typing_indicators_widget.dart index a791c1b56..3aae0e4b4 100644 --- a/commet/lib/ui/molecules/typing_indicators_widget.dart +++ b/commet/lib/ui/molecules/typing_indicators_widget.dart @@ -21,14 +21,6 @@ class _TypingIndicatorsWidgetState extends State { late List typingMembers; - late List blobKeys = [ - GlobalKey(), - GlobalKey(), - GlobalKey(), - ]; - - Timer? timer; - late String currentText = ""; String typingUsers(int howMany, String user1, String user2, String user3) => @@ -47,59 +39,25 @@ class _TypingIndicatorsWidgetState extends State { typingMembers = widget.component.typingUsers; if (typingMembers.isNotEmpty) { currentText = getTypingText(); - startTimer(); } super.initState(); } @override void dispose() { - timer?.cancel(); sub?.cancel(); super.dispose(); } - int prevIndex = 0; - - void onTimer(Timer timer) { - var r = Random().nextInt(3); - - if (r == prevIndex) { - r += 1; - r = r % blobKeys.length; - } - - prevIndex = r; - - var key = blobKeys[r]; - - if (key.currentState == null) { - return; - } - - var state = key.currentState! as __SingleTypingIndicatorBlobState; - state.controller.forward(from: 0); - } - void onTypingUsersUpdated(void event) { setState(() { typingMembers = widget.component.typingUsers; if (typingMembers.isNotEmpty) { currentText = getTypingText(); - if (timer == null) { - startTimer(); - } - } else { - timer?.cancel(); - timer = null; } }); } - void startTimer() { - timer = Timer.periodic(const Duration(milliseconds: 250), onTimer); - } - @override Widget build(BuildContext context) { return SizedBox( @@ -112,19 +70,7 @@ class _TypingIndicatorsWidgetState extends State { child: Row(children: [ Padding( padding: const EdgeInsets.fromLTRB(4, 0, 4, 0), - child: Row( - children: [ - _SingleTypingIndicatorBlob( - key: blobKeys[0], - ), - _SingleTypingIndicatorBlob( - key: blobKeys[1], - ), - _SingleTypingIndicatorBlob( - key: blobKeys[2], - ), - ], - ), + child: TypingIndicatorAnimation(), ), tiamat.Text.labelLow(currentText) ])), @@ -196,3 +142,73 @@ class __SingleTypingIndicatorBlobState extends State<_SingleTypingIndicatorBlob> ); } } + +class TypingIndicatorAnimation extends StatefulWidget { + const TypingIndicatorAnimation({super.key}); + + @override + State createState() => + _TypingIndicatorAnimationState(); +} + +class _TypingIndicatorAnimationState extends State { + late List blobKeys = [ + GlobalKey(), + GlobalKey(), + GlobalKey(), + ]; + + Timer? timer; + int prevIndex = 0; + + @override + void initState() { + super.initState(); + startTimer(); + } + + @override + void dispose() { + timer?.cancel(); + super.dispose(); + } + + void startTimer() { + timer = Timer.periodic(const Duration(milliseconds: 250), onTimer); + } + + void onTimer(Timer timer) { + var r = Random().nextInt(3); + + if (r == prevIndex) { + r += 1; + r = r % blobKeys.length; + } + + prevIndex = r; + + var key = blobKeys[r]; + + if (key.currentState == null) { + return; + } + + var state = key.currentState! as __SingleTypingIndicatorBlobState; + state.controller.forward(from: 0); + } + + @override + Widget build(BuildContext context) { + return Row(children: [ + _SingleTypingIndicatorBlob( + key: blobKeys[0], + ), + _SingleTypingIndicatorBlob( + key: blobKeys[1], + ), + _SingleTypingIndicatorBlob( + key: blobKeys[2], + ), + ]); + } +}