diff --git a/CHANGELOG.md b/CHANGELOG.md index 94de973..c9e845a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 0.1.6 +- Improved: Show typing indicator also in the chat overview - Fix: Phantom push notification - Fix: Smaller UI fixes diff --git a/lib/src/views/chats/chat_list_components/group_list_item.dart b/lib/src/views/chats/chat_list_components/group_list_item.dart index 32fa02f..00ccc1c 100644 --- a/lib/src/views/chats/chat_list_components/group_list_item.dart +++ b/lib/src/views/chats/chat_list_components/group_list_item.dart @@ -13,6 +13,7 @@ import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/chats/chat_list_components/last_message_time.dart'; +import 'package:twonly/src/views/chats/chat_list_components/typing_indicator_subtitle.dart'; import 'package:twonly/src/views/chats/chat_messages_components/message_send_state_icon.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/flame.dart'; @@ -251,6 +252,9 @@ class _UserListItem extends State { ) : Row( children: [ + TypingIndicatorSubtitle( + groupId: widget.group.groupId, + ), MessageSendStateIcon( _previewMessages, _previewMediaFiles, diff --git a/lib/src/views/chats/chat_list_components/typing_indicator_subtitle.dart b/lib/src/views/chats/chat_list_components/typing_indicator_subtitle.dart new file mode 100644 index 0000000..ebebb82 --- /dev/null +++ b/lib/src/views/chats/chat_list_components/typing_indicator_subtitle.dart @@ -0,0 +1,75 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/views/chats/chat_messages.view.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/typing_indicator.dart'; + +class TypingIndicatorSubtitle extends StatefulWidget { + const TypingIndicatorSubtitle({required this.groupId, super.key}); + + final String groupId; + + @override + State createState() => + _TypingIndicatorSubtitleState(); +} + +class _TypingIndicatorSubtitleState extends State { + List _groupMembers = []; + + late StreamSubscription> membersSub; + + late Timer _periodicUpdate; + + @override + void initState() { + super.initState(); + + _periodicUpdate = Timer.periodic(const Duration(seconds: 1), (_) { + filterOpenUsers(_groupMembers); + }); + + final membersStream = twonlyDB.groupsDao.watchGroupMembers( + widget.groupId, + ); + membersSub = membersStream.listen((update) { + filterOpenUsers(update.map((m) => m.$2).toList()); + }); + } + + void filterOpenUsers(List input) { + setState(() { + _groupMembers = input.where(isTyping).toList(); + }); + } + + @override + void dispose() { + membersSub.cancel(); + _periodicUpdate.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (_groupMembers.isEmpty) return Container(); + return Padding( + padding: const EdgeInsets.only(right: 5), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 3), + decoration: BoxDecoration( + color: getMessageColor(true), + borderRadius: BorderRadius.circular(12), + ), + child: Transform.scale( + scale: 0.6, + child: const AnimatedTypingDots( + isTyping: true, + ), + ), + ), + ); + } +} diff --git a/lib/src/views/chats/chat_messages_components/typing_indicator.dart b/lib/src/views/chats/chat_messages_components/typing_indicator.dart index da1f725..620680a 100644 --- a/lib/src/views/chats/chat_messages_components/typing_indicator.dart +++ b/lib/src/views/chats/chat_messages_components/typing_indicator.dart @@ -9,6 +9,28 @@ import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/views/chats/chat_messages.view.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; +bool isTyping(GroupMember member) { + return member.lastTypeIndicator != null && + clock + .now() + .difference( + member.lastTypeIndicator!, + ) + .inSeconds <= + 2; +} + +bool hasChatOpen(GroupMember member) { + return member.lastChatOpened != null && + clock + .now() + .difference( + member.lastChatOpened!, + ) + .inSeconds <= + 6; +} + class TypingIndicator extends StatefulWidget { const TypingIndicator({required this.group, super.key}); @@ -18,10 +40,8 @@ class TypingIndicator extends StatefulWidget { State createState() => _TypingIndicatorState(); } -class _TypingIndicatorState extends State - with SingleTickerProviderStateMixin { +class _TypingIndicatorState extends State { late AnimationController _controller; - late List> _animations; List _groupMembers = []; @@ -43,7 +63,88 @@ class _TypingIndicatorState extends State membersSub = membersStream.listen((update) { filterOpenUsers(update.map((m) => m.$2).toList()); }); + } + void filterOpenUsers(List input) { + setState(() { + _groupMembers = input.where(hasChatOpen).toList(); + }); + } + + @override + void dispose() { + _controller.dispose(); + membersSub.cancel(); + _periodicUpdate.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (_groupMembers.isEmpty) return Container(); + + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + children: _groupMembers + .map( + (member) => Padding( + key: Key('typing_indicator_${member.contactId}'), + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (!widget.group.isDirectChat) + GestureDetector( + onTap: () => context.push( + Routes.profileContact(member.contactId), + ), + child: AvatarIcon( + contactId: member.contactId, + fontSize: 12, + ), + ), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: getMessageColor(true), + borderRadius: BorderRadius.circular(12), + ), + child: AnimatedTypingDots( + isTyping: isTyping(member), + ), + ), + Expanded(child: Container()), + ], + ), + ), + ) + .toList(), + ), + ), + ); + } +} + +class AnimatedTypingDots extends StatefulWidget { + const AnimatedTypingDots({required this.isTyping, super.key}); + + final bool isTyping; + + @override + State createState() => _AnimatedTypingDotsState(); +} + +class _AnimatedTypingDotsState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + + late List> _animations; + + @override + void initState() { _controller = AnimationController( duration: const Duration(milliseconds: 1000), vsync: this, @@ -77,93 +178,18 @@ class _TypingIndicatorState extends State ), ); }); - } - - void filterOpenUsers(List input) { - setState(() { - _groupMembers = input.where(hasChatOpen).toList(); - }); - } - - @override - void dispose() { - _controller.dispose(); - membersSub.cancel(); - _periodicUpdate.cancel(); - super.dispose(); - } - - bool isTyping(GroupMember member) { - return member.lastTypeIndicator != null && - clock - .now() - .difference( - member.lastTypeIndicator!, - ) - .inSeconds <= - 2; - } - - bool hasChatOpen(GroupMember member) { - return member.lastChatOpened != null && - clock - .now() - .difference( - member.lastChatOpened!, - ) - .inSeconds <= - 6; + super.initState(); } @override Widget build(BuildContext context) { - if (_groupMembers.isEmpty) return Container(); - - return Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - children: _groupMembers - .map( - (member) => Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (!widget.group.isDirectChat) - GestureDetector( - onTap: () => context.push( - Routes.profileContact(member.contactId), - ), - child: AvatarIcon( - contactId: member.contactId, - fontSize: 12, - ), - ), - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: getMessageColor(true), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: List.generate( - 3, - (index) => _AnimatedDot( - isTyping: isTyping(member), - animation: _animations[index], - ), - ), - ), - ), - Expanded(child: Container()), - ], - ), - ), - ) - .toList(), + return Row( + mainAxisSize: MainAxisSize.min, + children: List.generate( + 3, + (index) => _AnimatedDot( + isTyping: widget.isTyping, + animation: _animations[index], ), ), );