From c7806abd61a16437ce0590eeb85709bc66fbf949 Mon Sep 17 00:00:00 2001 From: Krille Fear Date: Sun, 14 Nov 2021 09:36:35 +0100 Subject: [PATCH] feat: Display typing indicators with gif --- assets/typing.gif | Bin 0 -> 11536 bytes lib/pages/chat/chat_app_bar_title.dart | 58 +++++++ lib/pages/chat/chat_view.dart | 214 +++++++++---------------- lib/pages/chat/typing_indicators.dart | 98 +++++++++++ 4 files changed, 235 insertions(+), 135 deletions(-) create mode 100644 assets/typing.gif create mode 100644 lib/pages/chat/chat_app_bar_title.dart create mode 100644 lib/pages/chat/typing_indicators.dart diff --git a/assets/typing.gif b/assets/typing.gif new file mode 100644 index 0000000000000000000000000000000000000000..5beabb0ad25ee5a49022f227a1259084ce3245ee GIT binary patch literal 11536 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 B^+o^y literal 0 HcmV?d00001 diff --git a/lib/pages/chat/chat_app_bar_title.dart b/lib/pages/chat/chat_app_bar_title.dart new file mode 100644 index 000000000..1d8129f64 --- /dev/null +++ b/lib/pages/chat/chat_app_bar_title.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:vrouter/vrouter.dart'; + +import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart'; +import 'package:fluffychat/utils/room_status_extension.dart'; +import 'package:fluffychat/utils/stream_extension.dart'; +import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class ChatAppBarTitle extends StatelessWidget { + final ChatController controller; + const ChatAppBarTitle(this.controller, {Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + if (controller.selectedEvents.isNotEmpty) { + return Text(controller.selectedEvents.length.toString()); + } + return ListTile( + leading: Avatar(controller.room.avatar, controller.room.displayname), + contentPadding: EdgeInsets.zero, + onTap: controller.room.isDirectChat + ? () => showModalBottomSheet( + context: context, + builder: (c) => UserBottomSheet( + user: controller.room + .getUserByMXIDSync(controller.room.directChatMatrixID), + outerContext: context, + onMention: () => controller.sendController.text += + '${controller.room.getUserByMXIDSync(controller.room.directChatMatrixID).mention} ', + ), + ) + : () => VRouter.of(context) + .toSegments(['rooms', controller.room.id, 'details']), + title: Text( + controller.room + .getLocalizedDisplayname(MatrixLocals(L10n.of(context))), + maxLines: 1), + subtitle: StreamBuilder( + stream: Matrix.of(context) + .client + .onPresence + .stream + .where((p) => p.senderId == controller.room.directChatMatrixID) + .rateLimit(const Duration(seconds: 1)), + builder: (context, snapshot) => Text( + controller.room.getLocalizedStatus(context), + maxLines: 1, + //overflow: TextOverflow.ellipsis, + ), + ), + ); + } +} diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 65ad88f91..9d5ab3a77 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -13,15 +13,14 @@ import 'package:vrouter/vrouter.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pages/chat/chat_app_bar_title.dart'; import 'package:fluffychat/pages/chat/reactions_picker.dart'; import 'package:fluffychat/pages/chat/reply_display.dart'; import 'package:fluffychat/pages/chat/seen_by_row.dart'; import 'package:fluffychat/pages/chat/tombstone_display.dart'; +import 'package:fluffychat/pages/chat/typing_indicators.dart'; import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart'; import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/utils/room_status_extension.dart'; -import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/chat_settings_popup_menu.dart'; import 'package:fluffychat/widgets/connection_status_header.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -38,6 +37,74 @@ class ChatView extends StatelessWidget { const ChatView(this.controller, {Key key}) : super(key: key); + List _appBarActions(BuildContext context) => controller.selectMode + ? [ + if (controller.canEditSelectedEvents) + IconButton( + icon: const Icon(Icons.edit_outlined), + tooltip: L10n.of(context).edit, + onPressed: controller.editSelectedEventAction, + ), + IconButton( + icon: const Icon(Icons.copy_outlined), + tooltip: L10n.of(context).copy, + onPressed: controller.copyEventsAction, + ), + if (controller.canRedactSelectedEvents) + IconButton( + icon: const Icon(Icons.delete_outlined), + tooltip: L10n.of(context).redactMessage, + onPressed: controller.redactEventsAction, + ), + if (controller.selectedEvents.length == 1) + PopupMenuButton<_EventContextAction>( + onSelected: (action) { + switch (action) { + case _EventContextAction.info: + controller.showEventInfo(); + controller.clearSelectedEvents(); + break; + case _EventContextAction.report: + controller.reportEventAction(); + break; + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: _EventContextAction.info, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.info_outlined), + const SizedBox(width: 12), + Text(L10n.of(context).messageInfo), + ], + ), + ), + PopupMenuItem( + value: _EventContextAction.report, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.report_outlined), + const SizedBox(width: 12), + Text(L10n.of(context).reportMessage), + ], + ), + ), + ], + ), + ] + : [ + if (controller.room.canSendDefaultStates) + IconButton( + tooltip: L10n.of(context).videoCall, + icon: const Icon(Icons.video_call_outlined), + onPressed: controller.startCallAction, + ), + ChatSettingsPopupMenu(controller.room, !controller.room.isDirectChat), + ]; + @override Widget build(BuildContext context) { controller.matrix ??= Matrix.of(context); @@ -87,137 +154,8 @@ class ChatView extends StatelessWidget { ) : UnreadBadgeBackButton(roomId: controller.roomId), titleSpacing: 0, - title: controller.selectedEvents.isEmpty - ? ListTile( - leading: Avatar( - controller.room.avatar, controller.room.displayname), - contentPadding: EdgeInsets.zero, - onTap: controller.room.isDirectChat - ? () => showModalBottomSheet( - context: context, - builder: (c) => UserBottomSheet( - user: controller.room.getUserByMXIDSync( - controller.room.directChatMatrixID), - outerContext: context, - onMention: () => controller - .sendController.text += - '${controller.room.getUserByMXIDSync(controller.room.directChatMatrixID).mention} ', - ), - ) - : () => VRouter.of(context).toSegments( - ['rooms', controller.room.id, 'details']), - title: Text( - controller.room.getLocalizedDisplayname( - MatrixLocals(L10n.of(context))), - maxLines: 1), - subtitle: controller.room - .getLocalizedTypingText(context) - .isEmpty - ? StreamBuilder( - stream: Matrix.of(context) - .client - .onPresence - .stream - .where((p) => - p.senderId == - controller.room.directChatMatrixID) - .rateLimit(const Duration(seconds: 1)), - builder: (context, snapshot) => Text( - controller.room.getLocalizedStatus(context), - maxLines: 1, - //overflow: TextOverflow.ellipsis, - )) - : Row( - children: [ - Icon(Icons.edit_outlined, - color: - Theme.of(context).colorScheme.secondary, - size: 13), - const SizedBox(width: 4), - Expanded( - child: Text( - controller.room - .getLocalizedTypingText(context), - maxLines: 1, - style: TextStyle( - color: - Theme.of(context).colorScheme.secondary, - fontStyle: FontStyle.italic, - ), - ), - ), - ], - ), - ) - : Text(controller.selectedEvents.length.toString()), - actions: controller.selectMode - ? [ - if (controller.canEditSelectedEvents) - IconButton( - icon: const Icon(Icons.edit_outlined), - tooltip: L10n.of(context).edit, - onPressed: controller.editSelectedEventAction, - ), - IconButton( - icon: const Icon(Icons.copy_outlined), - tooltip: L10n.of(context).copy, - onPressed: controller.copyEventsAction, - ), - if (controller.canRedactSelectedEvents) - IconButton( - icon: const Icon(Icons.delete_outlined), - tooltip: L10n.of(context).redactMessage, - onPressed: controller.redactEventsAction, - ), - if (controller.selectedEvents.length == 1) - PopupMenuButton<_EventContextAction>( - onSelected: (action) { - switch (action) { - case _EventContextAction.info: - controller.showEventInfo(); - controller.clearSelectedEvents(); - break; - case _EventContextAction.report: - controller.reportEventAction(); - break; - } - }, - itemBuilder: (context) => [ - PopupMenuItem( - value: _EventContextAction.info, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.info_outlined), - const SizedBox(width: 12), - Text(L10n.of(context).messageInfo), - ], - ), - ), - PopupMenuItem( - value: _EventContextAction.report, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.report_outlined), - const SizedBox(width: 12), - Text(L10n.of(context).reportMessage), - ], - ), - ), - ], - ), - ] - : [ - if (controller.room.canSendDefaultStates) - IconButton( - tooltip: L10n.of(context).videoCall, - icon: const Icon(Icons.video_call_outlined), - onPressed: controller.startCallAction, - ), - ChatSettingsPopupMenu( - controller.room, !controller.room.isDirectChat), - ], + title: ChatAppBarTitle(controller), + actions: _appBarActions(context), ), floatingActionButton: controller.showScrollDownButton ? Padding( @@ -300,7 +238,13 @@ class ChatView extends StatelessWidget { ) : Container() : i == 0 - ? SeenByRow(controller) + ? Column( + mainAxisSize: MainAxisSize.min, + children: [ + SeenByRow(controller), + TypingIndicators(controller), + ], + ) : AutoScrollTag( key: ValueKey(controller .filteredEvents[i - 1].eventId), diff --git a/lib/pages/chat/typing_indicators.dart b/lib/pages/chat/typing_indicators.dart new file mode 100644 index 000000000..9769aecc1 --- /dev/null +++ b/lib/pages/chat/typing_indicators.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class TypingIndicators extends StatelessWidget { + final ChatController controller; + const TypingIndicators(this.controller, {Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final typingUsers = controller.room.typingUsers + ..removeWhere((u) => u.stateKey == Matrix.of(context).client.userID); + const topPadding = 24.0; + const bottomPadding = 4.0; + + return Container( + width: double.infinity, + alignment: Alignment.center, + child: AnimatedContainer( + constraints: + const BoxConstraints(maxWidth: FluffyThemes.columnWidth * 2.5), + height: typingUsers.isEmpty + ? 0 + : Avatar.defaultSize + bottomPadding + topPadding, + duration: const Duration(milliseconds: 300), + curve: Curves.bounceInOut, + alignment: controller.filteredEvents.isNotEmpty && + controller.filteredEvents.first.senderId == + Matrix.of(context).client.userID + ? Alignment.topRight + : Alignment.topLeft, + clipBehavior: Clip.hardEdge, + decoration: const BoxDecoration(), + padding: EdgeInsets.only( + left: typingUsers.length < 2 ? 8 : 0, + bottom: bottomPadding, + ), + child: Row( + children: [ + SizedBox( + height: Avatar.defaultSize, + width: typingUsers.length < 2 + ? Avatar.defaultSize + : Avatar.defaultSize + 8, + child: Stack( + children: [ + if (typingUsers.isNotEmpty) + Avatar( + typingUsers.first.avatarUrl, + typingUsers.first.calcDisplayname(), + ), + if (typingUsers.length == 2) + Padding( + padding: const EdgeInsets.only(left: 8), + child: Avatar( + typingUsers.length == 2 + ? typingUsers.last.avatarUrl + : null, + typingUsers.length == 2 + ? typingUsers.last.calcDisplayname() + : '+${typingUsers.length - 1}', + ), + ), + ], + ), + ), + const SizedBox(width: 8), + Padding( + padding: const EdgeInsets.only(top: topPadding), + child: Material( + color: Theme.of(context).backgroundColor, + elevation: 6, + shadowColor: + Theme.of(context).secondaryHeaderColor.withAlpha(100), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(2), + topRight: Radius.circular(AppConfig.borderRadius), + bottomLeft: Radius.circular(AppConfig.borderRadius), + bottomRight: Radius.circular(AppConfig.borderRadius), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: typingUsers.isEmpty + ? null + : Image.asset('assets/typing.gif', height: 12), + ), + ), + ), + ], + ), + ), + ); + } +}