From 24b632fc90a787c568207410588f65135e302b6d Mon Sep 17 00:00:00 2001 From: Steef Hegeman Date: Wed, 26 May 2021 20:28:08 +0200 Subject: [PATCH 1/2] back button clears selection: room list and chat When a room or event is selected and the Android back button is pressed, don't pop but clear the current selection. Fixes #399. --- lib/pages/views/chat_list_view.dart | 12 ++++++++++-- lib/pages/views/chat_view.dart | 12 ++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/pages/views/chat_list_view.dart b/lib/pages/views/chat_list_view.dart index 9880f3e4..83961429 100644 --- a/lib/pages/views/chat_list_view.dart +++ b/lib/pages/views/chat_list_view.dart @@ -26,7 +26,15 @@ class ChatListView extends StatelessWidget { : controller.selectedRoomIds.isEmpty ? SelectMode.normal : SelectMode.select; - return Scaffold( + return VWidgetGuard( + onSystemPop: (redirector) async { + if (controller.selectedRoomIds.isNotEmpty) { + controller.cancelAction(); + redirector.stopRedirection(); + } + }, + child: + Scaffold( appBar: AppBar( elevation: MediaQuery.of(context).size.width > FluffyThemes.columnWidth * 2 @@ -231,7 +239,7 @@ class ChatListView extends StatelessWidget { child: Icon(CupertinoIcons.chat_bubble), ) : null, - ); + )); }); } } diff --git a/lib/pages/views/chat_view.dart b/lib/pages/views/chat_view.dart index ff0935ac..b4e65b43 100644 --- a/lib/pages/views/chat_view.dart +++ b/lib/pages/views/chat_view.dart @@ -54,7 +54,15 @@ class ChatView extends StatelessWidget { context: context, future: () => controller.room.join()); } - return Scaffold( + return VWidgetGuard( + onSystemPop: (redirector) async { + if (controller.selectedEvents.isNotEmpty) { + controller.clearSelectedEvents(); + redirector.stopRedirection(); + } + }, + child: + Scaffold( appBar: AppBar( leading: controller.selectMode ? IconButton( @@ -694,7 +702,7 @@ class ChatView extends StatelessWidget { ), ], ), - ); + )); } } From a67de58d9aec946f6825c5af189a1e9e1442fb4d Mon Sep 17 00:00:00 2001 From: Steef Hegeman Date: Wed, 26 May 2021 20:50:15 +0200 Subject: [PATCH 2/2] formatting --- lib/pages/views/chat_list_view.dart | 396 ++++---- lib/pages/views/chat_view.dart | 1310 ++++++++++++++------------- 2 files changed, 868 insertions(+), 838 deletions(-) diff --git a/lib/pages/views/chat_list_view.dart b/lib/pages/views/chat_list_view.dart index 83961429..57de43a9 100644 --- a/lib/pages/views/chat_list_view.dart +++ b/lib/pages/views/chat_list_view.dart @@ -27,219 +27,219 @@ class ChatListView extends StatelessWidget { ? SelectMode.normal : SelectMode.select; return VWidgetGuard( - onSystemPop: (redirector) async { + onSystemPop: (redirector) async { if (controller.selectedRoomIds.isNotEmpty) { - controller.cancelAction(); - redirector.stopRedirection(); + controller.cancelAction(); + redirector.stopRedirection(); } - }, - child: - Scaffold( - appBar: AppBar( - elevation: MediaQuery.of(context).size.width > - FluffyThemes.columnWidth * 2 - ? 1 - : null, - leading: selectMode == SelectMode.normal - ? null - : IconButton( - tooltip: L10n.of(context).cancel, - icon: Icon(Icons.close_outlined), - onPressed: controller.cancelAction, - ), - centerTitle: false, - actions: selectMode == SelectMode.share - ? null - : selectMode == SelectMode.select - ? [ - if (controller.selectedRoomIds.length == 1) - IconButton( - tooltip: L10n.of(context).toggleUnread, - icon: Icon(Matrix.of(context) - .client - .getRoomById( - controller.selectedRoomIds.single) - .isUnread - ? Icons.mark_chat_read_outlined - : Icons.mark_chat_unread_outlined), - onPressed: controller.toggleUnread, - ), - if (controller.selectedRoomIds.length == 1) - IconButton( - tooltip: L10n.of(context).toggleFavorite, - icon: Icon(Icons.push_pin_outlined), - onPressed: controller.toggleFavouriteRoom, - ), - if (controller.selectedRoomIds.length == 1) - IconButton( - icon: Icon(Matrix.of(context) + }, + child: Scaffold( + appBar: AppBar( + elevation: MediaQuery.of(context).size.width > + FluffyThemes.columnWidth * 2 + ? 1 + : null, + leading: selectMode == SelectMode.normal + ? null + : IconButton( + tooltip: L10n.of(context).cancel, + icon: Icon(Icons.close_outlined), + onPressed: controller.cancelAction, + ), + centerTitle: false, + actions: selectMode == SelectMode.share + ? null + : selectMode == SelectMode.select + ? [ + if (controller.selectedRoomIds.length == 1) + IconButton( + tooltip: L10n.of(context).toggleUnread, + icon: Icon(Matrix.of(context) .client .getRoomById( controller.selectedRoomIds.single) - .pushRuleState == - PushRuleState.notify - ? Icons.notifications_off_outlined - : Icons.notifications_outlined), - tooltip: L10n.of(context).toggleMuted, - onPressed: controller.toggleMuted, - ), - IconButton( - icon: Icon(Icons.archive_outlined), - tooltip: L10n.of(context).archive, - onPressed: controller.archiveAction, - ), - ] - : [ - IconButton( - icon: Icon(Icons.search_outlined), - tooltip: L10n.of(context).search, - onPressed: () => - VRouter.of(context).push('/search'), - ), - PopupMenuButton( - onSelected: controller.onPopupMenuSelect, - itemBuilder: (_) => [ - PopupMenuItem( - value: PopupMenuAction.setStatus, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.edit_outlined), - SizedBox(width: 12), - Text(L10n.of(context).setStatus), - ], + .isUnread + ? Icons.mark_chat_read_outlined + : Icons.mark_chat_unread_outlined), + onPressed: controller.toggleUnread, ), - ), - PopupMenuItem( - value: PopupMenuAction.newGroup, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.group_add_outlined), - SizedBox(width: 12), - Text(L10n.of(context).createNewGroup), - ], + if (controller.selectedRoomIds.length == 1) + IconButton( + tooltip: L10n.of(context).toggleFavorite, + icon: Icon(Icons.push_pin_outlined), + onPressed: controller.toggleFavouriteRoom, ), - ), - PopupMenuItem( - value: PopupMenuAction.invite, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.share_outlined), - SizedBox(width: 12), - Text(L10n.of(context).inviteContact), - ], + if (controller.selectedRoomIds.length == 1) + IconButton( + icon: Icon(Matrix.of(context) + .client + .getRoomById(controller + .selectedRoomIds.single) + .pushRuleState == + PushRuleState.notify + ? Icons.notifications_off_outlined + : Icons.notifications_outlined), + tooltip: L10n.of(context).toggleMuted, + onPressed: controller.toggleMuted, ), + IconButton( + icon: Icon(Icons.archive_outlined), + tooltip: L10n.of(context).archive, + onPressed: controller.archiveAction, ), - PopupMenuItem( - value: PopupMenuAction.archive, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.archive_outlined), - SizedBox(width: 12), - Text(L10n.of(context).archive), - ], - ), + ] + : [ + IconButton( + icon: Icon(Icons.search_outlined), + tooltip: L10n.of(context).search, + onPressed: () => + VRouter.of(context).push('/search'), ), - PopupMenuItem( - value: PopupMenuAction.settings, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.settings_outlined), - SizedBox(width: 12), - Text(L10n.of(context).settings), - ], - ), - ), - ], - ), - ], - title: Text(selectMode == SelectMode.share - ? L10n.of(context).share - : selectMode == SelectMode.select - ? L10n.of(context).numberSelected( - controller.selectedRoomIds.length.toString()) - : AppConfig.applicationName), - ), - body: Column(children: [ - ConnectionStatusHeader(), - Expanded( - child: StreamBuilder( - stream: Matrix.of(context) - .client - .onSync - .stream - .where((s) => s.hasRoomUpdate), - builder: (context, snapshot) { - return FutureBuilder( - future: controller.waitForFirstSync(), - builder: (BuildContext context, snapshot) { - if (snapshot.hasData) { - final rooms = List.from( - Matrix.of(context).client.rooms); - rooms.removeWhere((room) => room.lastEvent == null); - if (rooms.isEmpty) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.maps_ugc_outlined, - size: 80, - color: Colors.grey, + PopupMenuButton( + onSelected: controller.onPopupMenuSelect, + itemBuilder: (_) => [ + PopupMenuItem( + value: PopupMenuAction.setStatus, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.edit_outlined), + SizedBox(width: 12), + Text(L10n.of(context).setStatus), + ], + ), ), - Center( - child: Text( - L10n.of(context).startYourFirstChat, - textAlign: TextAlign.start, - style: TextStyle( - color: Colors.grey, - fontSize: 16, - ), + PopupMenuItem( + value: PopupMenuAction.newGroup, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.group_add_outlined), + SizedBox(width: 12), + Text(L10n.of(context).createNewGroup), + ], + ), + ), + PopupMenuItem( + value: PopupMenuAction.invite, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.share_outlined), + SizedBox(width: 12), + Text(L10n.of(context).inviteContact), + ], + ), + ), + PopupMenuItem( + value: PopupMenuAction.archive, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.archive_outlined), + SizedBox(width: 12), + Text(L10n.of(context).archive), + ], + ), + ), + PopupMenuItem( + value: PopupMenuAction.settings, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.settings_outlined), + SizedBox(width: 12), + Text(L10n.of(context).settings), + ], ), ), ], - ); - } - final totalCount = rooms.length; - return ListView.builder( - itemCount: totalCount, - itemBuilder: (BuildContext context, int i) => - ChatListItem( - rooms[i], - selected: controller.selectedRoomIds - .contains(rooms[i].id), - onTap: selectMode == SelectMode.select - ? () => - controller.toggleSelection(rooms[i].id) - : null, - onLongPress: () => - controller.toggleSelection(rooms[i].id), - activeChat: - controller.activeChat == rooms[i].id, ), - ); - } else { - return Center( - child: CircularProgressIndicator(), - ); - } - }, - ); - }), - ), - ]), - floatingActionButton: selectMode == SelectMode.normal - ? FloatingActionButton( - onPressed: () => - VRouter.of(context).push('/newprivatechat'), - child: Icon(CupertinoIcons.chat_bubble), - ) - : null, - )); + ], + title: Text(selectMode == SelectMode.share + ? L10n.of(context).share + : selectMode == SelectMode.select + ? L10n.of(context).numberSelected( + controller.selectedRoomIds.length.toString()) + : AppConfig.applicationName), + ), + body: Column(children: [ + ConnectionStatusHeader(), + Expanded( + child: StreamBuilder( + stream: Matrix.of(context) + .client + .onSync + .stream + .where((s) => s.hasRoomUpdate), + builder: (context, snapshot) { + return FutureBuilder( + future: controller.waitForFirstSync(), + builder: (BuildContext context, snapshot) { + if (snapshot.hasData) { + final rooms = List.from( + Matrix.of(context).client.rooms); + rooms.removeWhere( + (room) => room.lastEvent == null); + if (rooms.isEmpty) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.maps_ugc_outlined, + size: 80, + color: Colors.grey, + ), + Center( + child: Text( + L10n.of(context).startYourFirstChat, + textAlign: TextAlign.start, + style: TextStyle( + color: Colors.grey, + fontSize: 16, + ), + ), + ), + ], + ); + } + final totalCount = rooms.length; + return ListView.builder( + itemCount: totalCount, + itemBuilder: (BuildContext context, int i) => + ChatListItem( + rooms[i], + selected: controller.selectedRoomIds + .contains(rooms[i].id), + onTap: selectMode == SelectMode.select + ? () => controller + .toggleSelection(rooms[i].id) + : null, + onLongPress: () => + controller.toggleSelection(rooms[i].id), + activeChat: + controller.activeChat == rooms[i].id, + ), + ); + } else { + return Center( + child: CircularProgressIndicator(), + ); + } + }, + ); + }), + ), + ]), + floatingActionButton: selectMode == SelectMode.normal + ? FloatingActionButton( + onPressed: () => + VRouter.of(context).push('/newprivatechat'), + child: Icon(CupertinoIcons.chat_bubble), + ) + : null, + )); }); } } diff --git a/lib/pages/views/chat_view.dart b/lib/pages/views/chat_view.dart index b4e65b43..4cfa6848 100644 --- a/lib/pages/views/chat_view.dart +++ b/lib/pages/views/chat_view.dart @@ -55,654 +55,684 @@ class ChatView extends StatelessWidget { } return VWidgetGuard( - onSystemPop: (redirector) async { - if (controller.selectedEvents.isNotEmpty) { - controller.clearSelectedEvents(); - redirector.stopRedirection(); - } - }, - child: - Scaffold( - appBar: AppBar( - leading: controller.selectMode - ? IconButton( - icon: Icon(Icons.close), - onPressed: controller.clearSelectedEvents, - tooltip: L10n.of(context).close, - ) - : UnreadBadgeBackButton(roomId: controller.roomId), - titleSpacing: 0, - title: controller.selectedEvents.isEmpty - ? StreamBuilder( - stream: controller.room.onUpdate.stream, - builder: (context, snapshot) => 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.directChatMatrixID} ', - ), - ) - : () => VRouter.of(context) - .push('/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), - 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), - SizedBox(width: 4), - Expanded( - child: Text( - controller.room - .getLocalizedTypingText(context), - maxLines: 1, - style: TextStyle( - color: Theme.of(context) - .colorScheme - .secondary, - fontStyle: FontStyle.italic, + onSystemPop: (redirector) async { + if (controller.selectedEvents.isNotEmpty) { + controller.clearSelectedEvents(); + redirector.stopRedirection(); + } + }, + child: Scaffold( + appBar: AppBar( + leading: controller.selectMode + ? IconButton( + icon: Icon(Icons.close), + onPressed: controller.clearSelectedEvents, + tooltip: L10n.of(context).close, + ) + : UnreadBadgeBackButton(roomId: controller.roomId), + titleSpacing: 0, + title: controller.selectedEvents.isEmpty + ? StreamBuilder( + stream: controller.room.onUpdate.stream, + builder: (context, snapshot) => 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.directChatMatrixID} ', ), - ), - ), - ], - ), - )) - : Text(L10n.of(context) - .numberSelected(controller.selectedEvents.length.toString())), - actions: controller.selectMode - ? [ - if (controller.selectedEvents.length == 1 && - controller.selectedEvents.first.status > 0 && - controller.selectedEvents.first.senderId == client.userID) - IconButton( - icon: Icon(Icons.edit_outlined), - tooltip: L10n.of(context).edit, - onPressed: controller.editSelectedEventAction, - ), - PopupMenuButton( - onSelected: controller.onEventActionPopupMenuSelected, - itemBuilder: (_) => [ - PopupMenuItem( - value: 'copy', - child: Text(L10n.of(context).copy), - ), - if (controller.canRedactSelectedEvents) - PopupMenuItem( - value: 'redact', - child: Text( - L10n.of(context).redactMessage, - style: TextStyle(color: Colors.orange), - ), - ), - if (controller.selectedEvents.length == 1) - PopupMenuItem( - value: 'report', - child: Text( - L10n.of(context).reportMessage, - style: TextStyle(color: Colors.red), - ), - ), - ], - ), - ] - : [ - if (controller.room.canSendDefaultStates) - IconButton( - tooltip: L10n.of(context).videoCall, - icon: Icon(Icons.video_call_outlined), - onPressed: controller.startCallAction, - ), - ChatSettingsPopupMenu( - controller.room, !controller.room.isDirectChat), - ], - ), - floatingActionButton: controller.showScrollDownButton - ? Padding( - padding: const EdgeInsets.only(bottom: 56.0), - child: FloatingActionButton( - onPressed: controller.scrollDown, - foregroundColor: Theme.of(context).textTheme.bodyText2.color, - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - mini: true, - child: Icon(Icons.arrow_downward_outlined, - color: Theme.of(context).primaryColor), - ), - ) - : null, - body: Stack( - children: [ - if (Matrix.of(context).wallpaper != null) - Image.file( - Matrix.of(context).wallpaper, - width: double.infinity, - height: double.infinity, - fit: BoxFit.cover, - ), - SafeArea( - child: Column( - children: [ - ConnectionStatusHeader(), - if (controller.room.getState(EventTypes.RoomTombstone) != null) - Container( - height: 72, - child: Material( - color: Theme.of(context).secondaryHeaderColor, - child: ListTile( - leading: CircleAvatar( - foregroundColor: - Theme.of(context).colorScheme.secondary, - backgroundColor: Theme.of(context).backgroundColor, - child: Icon(Icons.upgrade_outlined), - ), - title: Text( - controller.room - .getState(EventTypes.RoomTombstone) - .parsedTombstoneContent - .body, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - subtitle: Text(L10n.of(context).goToTheNewRoom), - onTap: controller.goToNewRoomAction, - ), - ), - ), - Expanded( - child: FutureBuilder( - future: controller.getTimeline(), - builder: (BuildContext context, snapshot) { - if (controller.timeline == null) { - return Center( - child: CircularProgressIndicator(), - ); - } - - // create a map of eventId --> index to greatly improve performance of - // ListView's findChildIndexCallback - final thisEventsKeyMap = {}; - for (var i = 0; - i < controller.filteredEvents.length; - i++) { - thisEventsKeyMap[controller.filteredEvents[i].eventId] = - i; - } - - return ListView.custom( - padding: EdgeInsets.only( - top: 16, - bottom: 4, - ), - reverse: true, - controller: controller.scrollController, - childrenDelegate: SliverChildBuilderDelegate( - (BuildContext context, int i) { - return i == controller.filteredEvents.length + 1 - ? controller.timeline.isRequestingHistory - ? Container( - height: 50, - alignment: Alignment.center, - padding: EdgeInsets.all(8), - child: CircularProgressIndicator(), - ) - : controller.canLoadMore - ? TextButton( - onPressed: - controller.requestHistory, - child: Text( - L10n.of(context).loadMore, - style: TextStyle( - color: Theme.of(context) - .primaryColor, - fontWeight: FontWeight.bold, - decoration: - TextDecoration.underline, - ), - ), - ) - : Container() - : i == 0 - ? StreamBuilder( - stream: controller.room.onUpdate.stream, - builder: (_, __) { - final seenByText = controller.room - .getLocalizedSeenByText( - context, - controller.timeline, - controller.filteredEvents, - controller.unfolded, - ); - return AnimatedContainer( - height: seenByText.isEmpty ? 0 : 24, - duration: seenByText.isEmpty - ? Duration(milliseconds: 0) - : Duration(milliseconds: 300), - alignment: controller.filteredEvents - .first.senderId == - client.userID - ? Alignment.topRight - : Alignment.topLeft, - padding: EdgeInsets.only( - left: 8, - right: 8, - bottom: 8, - ), - child: Container( - padding: EdgeInsets.symmetric( - horizontal: 4), - decoration: BoxDecoration( - color: Theme.of(context) - .scaffoldBackgroundColor - .withOpacity(0.8), - borderRadius: - BorderRadius.circular(4), - ), - child: Text( - seenByText, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Theme.of(context) - .colorScheme - .secondary, - ), - ), - ), - ); - }, - ) - : AutoScrollTag( - key: ValueKey(controller - .filteredEvents[i - 1].eventId), - index: i - 1, - controller: controller.scrollController, - child: Swipeable( - key: ValueKey(controller - .filteredEvents[i - 1].eventId), - background: Padding( - padding: EdgeInsets.symmetric( - horizontal: 12.0), - child: Center( - child: Icon(Icons.reply_outlined), - ), - ), - direction: SwipeDirection.endToStart, - onSwipe: (direction) => - controller.replyAction( - replyTo: controller - .filteredEvents[i - 1]), - child: Message( - controller.filteredEvents[i - 1], - onAvatarTab: (Event event) => - showModalBottomSheet( - context: context, - builder: (c) => - UserBottomSheet( - user: event.sender, - outerContext: context, - onMention: () => controller - .sendController - .text += - '${event.senderId} ', - ), - ), - unfold: controller.unfold, - onSelect: - controller.onSelectMessage, - scrollToEventId: - (String eventId) => controller - .scrollToEventId(eventId), - longPressSelect: controller - .selectedEvents.isEmpty, - selected: controller - .selectedEvents - .contains(controller - .filteredEvents[i - 1]), - timeline: controller.timeline, - nextEvent: i >= 2 - ? controller - .filteredEvents[i - 2] - : null), + ) + : () => VRouter.of(context) + .push('/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), + 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), + SizedBox(width: 4), + Expanded( + child: Text( + controller.room + .getLocalizedTypingText(context), + maxLines: 1, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .secondary, + fontStyle: FontStyle.italic, ), - ); - }, - childCount: controller.filteredEvents.length + 2, - findChildIndexCallback: (key) => controller - .findChildIndexCallback(key, thisEventsKeyMap), - ), - ); - }, - ), - ), - if (!controller.showEmojiPicker) - AnimatedContainer( - duration: Duration(milliseconds: 300), - height: (controller.editEvent == null && - controller.replyEvent == null && - controller.room.canSendDefaultMessages && - controller.selectedEvents.length == 1) - ? 56 - : 0, - child: Material( - color: Theme.of(context).secondaryHeaderColor, - child: Builder(builder: (context) { - if (!(controller.editEvent == null && - controller.replyEvent == null && - controller.selectedEvents.length == 1)) { - return Container(); - } - final emojis = List.from(AppEmojis.emojis); - final allReactionEvents = controller - .selectedEvents.first - .aggregatedEvents( - controller.timeline, RelationshipTypes.reaction) - ?.where((event) => - event.senderId == event.room.client.userID && - event.type == 'm.reaction'); - - allReactionEvents.forEach((event) { - try { - emojis.remove(event.content['m.relates_to']['key']); - } catch (_) {} - }); - return ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: emojis.length + 1, - itemBuilder: (c, i) => i == emojis.length - ? InkWell( - borderRadius: BorderRadius.circular(8), - onTap: () => controller - .pickEmojiAction(allReactionEvents), - child: Container( - width: 56, - height: 56, - alignment: Alignment.center, - child: Icon(Icons.add_outlined), - ), - ) - : InkWell( - borderRadius: BorderRadius.circular(8), - onTap: () => - controller.sendEmojiAction(emojis[i]), - child: Container( - width: 56, - height: 56, - alignment: Alignment.center, - child: Text( - emojis[i], - style: TextStyle(fontSize: 30), - ), - ), - ), - ); - }), - ), - ), - AnimatedContainer( - duration: Duration(milliseconds: 300), - height: controller.editEvent != null || - controller.replyEvent != null - ? 56 - : 0, - child: Material( - color: Theme.of(context).secondaryHeaderColor, - child: Row( - children: [ - IconButton( - tooltip: L10n.of(context).close, - icon: Icon(Icons.close), - onPressed: controller.cancelReplyEventAction, - ), - Expanded( - child: controller.replyEvent != null - ? ReplyContent(controller.replyEvent, - timeline: controller.timeline) - : _EditContent(controller.editEvent - ?.getDisplayEvent(controller.timeline)), - ), - ], - ), - ), - ), - Divider( - height: 1, - thickness: 1, - ), - if (controller.room.canSendDefaultMessages && - controller.room.membership == Membership.join && - !controller.showEmojiPicker) - Container( - decoration: BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: controller.selectMode - ? [ - Container( - height: 56, - child: TextButton( - onPressed: controller.forwardEventsAction, - child: Row( - children: [ - Icon(Icons.keyboard_arrow_left_outlined), - Text(L10n.of(context).forward), - ], - ), - ), - ), - controller.selectedEvents.length == 1 - ? controller.selectedEvents.first - .getDisplayEvent( - controller.timeline) - .status > - 0 - ? Container( - height: 56, - child: TextButton( - onPressed: controller.replyAction, - child: Row( - children: [ - Text(L10n.of(context).reply), - Icon( - Icons.keyboard_arrow_right), - ], - ), - ), - ) - : Container( - height: 56, - child: TextButton( - onPressed: - controller.sendAgainAction, - child: Row( - children: [ - Text(L10n.of(context) - .tryToSendAgain), - SizedBox(width: 4), - Icon(Icons.send_outlined, - size: 16), - ], - ), - ), - ) - : Container(), - ] - : [ - AnimatedContainer( - duration: Duration(milliseconds: 200), - height: 56, - width: controller.inputText.isEmpty ? 56 : 0, - alignment: Alignment.center, - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration(), - child: PopupMenuButton( - icon: Icon(Icons.add_outlined), - onSelected: - controller.onAddPopupMenuButtonSelected, - itemBuilder: (BuildContext context) => - >[ - PopupMenuItem( - value: 'file', - child: ListTile( - leading: CircleAvatar( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - child: - Icon(Icons.attachment_outlined), - ), - title: Text(L10n.of(context).sendFile), - contentPadding: EdgeInsets.all(0), ), ), - PopupMenuItem( - value: 'image', - child: ListTile( - leading: CircleAvatar( - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - child: Icon(Icons.image_outlined), - ), - title: Text(L10n.of(context).sendImage), - contentPadding: EdgeInsets.all(0), - ), - ), - if (PlatformInfos.isMobile) - PopupMenuItem( - value: 'camera', - child: ListTile( - leading: CircleAvatar( - backgroundColor: Colors.purple, - foregroundColor: Colors.white, - child: - Icon(Icons.camera_alt_outlined), - ), - title: - Text(L10n.of(context).openCamera), - contentPadding: EdgeInsets.all(0), - ), - ), - if (PlatformInfos.isMobile) - PopupMenuItem( - value: 'voice', - child: ListTile( - leading: CircleAvatar( - backgroundColor: Colors.red, - foregroundColor: Colors.white, - child: - Icon(Icons.mic_none_outlined), - ), - title: Text( - L10n.of(context).voiceMessage), - contentPadding: EdgeInsets.all(0), - ), - ), ], ), - ), - Container( - height: 56, - alignment: Alignment.center, - child: EncryptionButton(controller.room), - ), - Expanded( - child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 4.0), - child: InputBar( - room: controller.room, - minLines: 1, - maxLines: kIsWeb ? 1 : 8, - autofocus: !PlatformInfos.isMobile, - keyboardType: !PlatformInfos.isMobile - ? TextInputType.text - : TextInputType.multiline, - onSubmitted: controller.onInputBarSubmitted, - focusNode: controller.inputFocus, - controller: controller.sendController, - decoration: InputDecoration( - hintText: L10n.of(context).writeAMessage, - hintMaxLines: 1, - border: InputBorder.none, - enabledBorder: InputBorder.none, - filled: false, - ), - onChanged: controller.onInputBarChanged, - ), - ), - ), - if (PlatformInfos.isMobile && - controller.inputText.isEmpty) - Container( - height: 56, - alignment: Alignment.center, - child: IconButton( - tooltip: L10n.of(context).voiceMessage, - icon: Icon(Icons.mic_none_outlined), - onPressed: controller.voiceMessageAction, - ), - ), - if (!PlatformInfos.isMobile || - controller.inputText.isNotEmpty) - Container( - height: 56, - alignment: Alignment.center, - child: IconButton( - icon: Icon(Icons.send_outlined), - onPressed: controller.send, - tooltip: L10n.of(context).send, - ), - ), - ], + )) + : Text(L10n.of(context).numberSelected( + controller.selectedEvents.length.toString())), + actions: controller.selectMode + ? [ + if (controller.selectedEvents.length == 1 && + controller.selectedEvents.first.status > 0 && + controller.selectedEvents.first.senderId == + client.userID) + IconButton( + icon: Icon(Icons.edit_outlined), + tooltip: L10n.of(context).edit, + onPressed: controller.editSelectedEventAction, + ), + PopupMenuButton( + onSelected: controller.onEventActionPopupMenuSelected, + itemBuilder: (_) => [ + PopupMenuItem( + value: 'copy', + child: Text(L10n.of(context).copy), + ), + if (controller.canRedactSelectedEvents) + PopupMenuItem( + value: 'redact', + child: Text( + L10n.of(context).redactMessage, + style: TextStyle(color: Colors.orange), + ), + ), + if (controller.selectedEvents.length == 1) + PopupMenuItem( + value: 'report', + child: Text( + L10n.of(context).reportMessage, + style: TextStyle(color: Colors.red), + ), + ), + ], ), - ), - AnimatedContainer( - duration: Duration(milliseconds: 300), - height: controller.showEmojiPicker - ? MediaQuery.of(context).size.height / 2 - : 0, - child: controller.showEmojiPicker - ? EmojiPicker( - onEmojiSelected: controller.onEmojiSelected, - onBackspacePressed: controller.cancelEmojiPicker, - ) - : null, - ), - ], - ), + ] + : [ + if (controller.room.canSendDefaultStates) + IconButton( + tooltip: L10n.of(context).videoCall, + icon: Icon(Icons.video_call_outlined), + onPressed: controller.startCallAction, + ), + ChatSettingsPopupMenu( + controller.room, !controller.room.isDirectChat), + ], ), - ], - ), - )); + floatingActionButton: controller.showScrollDownButton + ? Padding( + padding: const EdgeInsets.only(bottom: 56.0), + child: FloatingActionButton( + onPressed: controller.scrollDown, + foregroundColor: + Theme.of(context).textTheme.bodyText2.color, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + mini: true, + child: Icon(Icons.arrow_downward_outlined, + color: Theme.of(context).primaryColor), + ), + ) + : null, + body: Stack( + children: [ + if (Matrix.of(context).wallpaper != null) + Image.file( + Matrix.of(context).wallpaper, + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + ), + SafeArea( + child: Column( + children: [ + ConnectionStatusHeader(), + if (controller.room.getState(EventTypes.RoomTombstone) != + null) + Container( + height: 72, + child: Material( + color: Theme.of(context).secondaryHeaderColor, + child: ListTile( + leading: CircleAvatar( + foregroundColor: + Theme.of(context).colorScheme.secondary, + backgroundColor: + Theme.of(context).backgroundColor, + child: Icon(Icons.upgrade_outlined), + ), + title: Text( + controller.room + .getState(EventTypes.RoomTombstone) + .parsedTombstoneContent + .body, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text(L10n.of(context).goToTheNewRoom), + onTap: controller.goToNewRoomAction, + ), + ), + ), + Expanded( + child: FutureBuilder( + future: controller.getTimeline(), + builder: (BuildContext context, snapshot) { + if (controller.timeline == null) { + return Center( + child: CircularProgressIndicator(), + ); + } + + // create a map of eventId --> index to greatly improve performance of + // ListView's findChildIndexCallback + final thisEventsKeyMap = {}; + for (var i = 0; + i < controller.filteredEvents.length; + i++) { + thisEventsKeyMap[ + controller.filteredEvents[i].eventId] = i; + } + + return ListView.custom( + padding: EdgeInsets.only( + top: 16, + bottom: 4, + ), + reverse: true, + controller: controller.scrollController, + childrenDelegate: SliverChildBuilderDelegate( + (BuildContext context, int i) { + return i == controller.filteredEvents.length + 1 + ? controller.timeline.isRequestingHistory + ? Container( + height: 50, + alignment: Alignment.center, + padding: EdgeInsets.all(8), + child: CircularProgressIndicator(), + ) + : controller.canLoadMore + ? TextButton( + onPressed: + controller.requestHistory, + child: Text( + L10n.of(context).loadMore, + style: TextStyle( + color: Theme.of(context) + .primaryColor, + fontWeight: FontWeight.bold, + decoration: TextDecoration + .underline, + ), + ), + ) + : Container() + : i == 0 + ? StreamBuilder( + stream: + controller.room.onUpdate.stream, + builder: (_, __) { + final seenByText = controller.room + .getLocalizedSeenByText( + context, + controller.timeline, + controller.filteredEvents, + controller.unfolded, + ); + return AnimatedContainer( + height: + seenByText.isEmpty ? 0 : 24, + duration: seenByText.isEmpty + ? Duration(milliseconds: 0) + : Duration( + milliseconds: 300), + alignment: controller + .filteredEvents + .first + .senderId == + client.userID + ? Alignment.topRight + : Alignment.topLeft, + padding: EdgeInsets.only( + left: 8, + right: 8, + bottom: 8, + ), + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 4), + decoration: BoxDecoration( + color: Theme.of(context) + .scaffoldBackgroundColor + .withOpacity(0.8), + borderRadius: + BorderRadius.circular( + 4), + ), + child: Text( + seenByText, + maxLines: 1, + overflow: + TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .secondary, + ), + ), + ), + ); + }, + ) + : AutoScrollTag( + key: ValueKey(controller + .filteredEvents[i - 1].eventId), + index: i - 1, + controller: + controller.scrollController, + child: Swipeable( + key: ValueKey(controller + .filteredEvents[i - 1] + .eventId), + background: Padding( + padding: EdgeInsets.symmetric( + horizontal: 12.0), + child: Center( + child: Icon( + Icons.reply_outlined), + ), + ), + direction: + SwipeDirection.endToStart, + onSwipe: (direction) => + controller.replyAction( + replyTo: controller + .filteredEvents[ + i - 1]), + child: Message( + controller + .filteredEvents[i - 1], + onAvatarTab: (Event event) => + showModalBottomSheet( + context: context, + builder: (c) => + UserBottomSheet( + user: event.sender, + outerContext: context, + onMention: () => controller + .sendController + .text += + '${event.senderId} ', + ), + ), + unfold: controller.unfold, + onSelect: controller + .onSelectMessage, + scrollToEventId: (String eventId) => + controller.scrollToEventId( + eventId), + longPressSelect: controller + .selectedEvents.isEmpty, + selected: controller + .selectedEvents + .contains( + controller.filteredEvents[ + i - 1]), + timeline: controller.timeline, + nextEvent: i >= 2 + ? controller.filteredEvents[i - 2] + : null), + ), + ); + }, + childCount: controller.filteredEvents.length + 2, + findChildIndexCallback: (key) => + controller.findChildIndexCallback( + key, thisEventsKeyMap), + ), + ); + }, + ), + ), + if (!controller.showEmojiPicker) + AnimatedContainer( + duration: Duration(milliseconds: 300), + height: (controller.editEvent == null && + controller.replyEvent == null && + controller.room.canSendDefaultMessages && + controller.selectedEvents.length == 1) + ? 56 + : 0, + child: Material( + color: Theme.of(context).secondaryHeaderColor, + child: Builder(builder: (context) { + if (!(controller.editEvent == null && + controller.replyEvent == null && + controller.selectedEvents.length == 1)) { + return Container(); + } + final emojis = List.from(AppEmojis.emojis); + final allReactionEvents = controller + .selectedEvents.first + .aggregatedEvents(controller.timeline, + RelationshipTypes.reaction) + ?.where((event) => + event.senderId == + event.room.client.userID && + event.type == 'm.reaction'); + + allReactionEvents.forEach((event) { + try { + emojis.remove( + event.content['m.relates_to']['key']); + } catch (_) {} + }); + return ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: emojis.length + 1, + itemBuilder: (c, i) => i == emojis.length + ? InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () => controller + .pickEmojiAction(allReactionEvents), + child: Container( + width: 56, + height: 56, + alignment: Alignment.center, + child: Icon(Icons.add_outlined), + ), + ) + : InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () => + controller.sendEmojiAction(emojis[i]), + child: Container( + width: 56, + height: 56, + alignment: Alignment.center, + child: Text( + emojis[i], + style: TextStyle(fontSize: 30), + ), + ), + ), + ); + }), + ), + ), + AnimatedContainer( + duration: Duration(milliseconds: 300), + height: controller.editEvent != null || + controller.replyEvent != null + ? 56 + : 0, + child: Material( + color: Theme.of(context).secondaryHeaderColor, + child: Row( + children: [ + IconButton( + tooltip: L10n.of(context).close, + icon: Icon(Icons.close), + onPressed: controller.cancelReplyEventAction, + ), + Expanded( + child: controller.replyEvent != null + ? ReplyContent(controller.replyEvent, + timeline: controller.timeline) + : _EditContent(controller.editEvent + ?.getDisplayEvent(controller.timeline)), + ), + ], + ), + ), + ), + Divider( + height: 1, + thickness: 1, + ), + if (controller.room.canSendDefaultMessages && + controller.room.membership == Membership.join && + !controller.showEmojiPicker) + Container( + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: controller.selectMode + ? [ + Container( + height: 56, + child: TextButton( + onPressed: controller.forwardEventsAction, + child: Row( + children: [ + Icon(Icons + .keyboard_arrow_left_outlined), + Text(L10n.of(context).forward), + ], + ), + ), + ), + controller.selectedEvents.length == 1 + ? controller.selectedEvents.first + .getDisplayEvent( + controller.timeline) + .status > + 0 + ? Container( + height: 56, + child: TextButton( + onPressed: + controller.replyAction, + child: Row( + children: [ + Text( + L10n.of(context).reply), + Icon(Icons + .keyboard_arrow_right), + ], + ), + ), + ) + : Container( + height: 56, + child: TextButton( + onPressed: + controller.sendAgainAction, + child: Row( + children: [ + Text(L10n.of(context) + .tryToSendAgain), + SizedBox(width: 4), + Icon(Icons.send_outlined, + size: 16), + ], + ), + ), + ) + : Container(), + ] + : [ + AnimatedContainer( + duration: Duration(milliseconds: 200), + height: 56, + width: + controller.inputText.isEmpty ? 56 : 0, + alignment: Alignment.center, + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration(), + child: PopupMenuButton( + icon: Icon(Icons.add_outlined), + onSelected: controller + .onAddPopupMenuButtonSelected, + itemBuilder: (BuildContext context) => + >[ + PopupMenuItem( + value: 'file', + child: ListTile( + leading: CircleAvatar( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + child: Icon( + Icons.attachment_outlined), + ), + title: + Text(L10n.of(context).sendFile), + contentPadding: EdgeInsets.all(0), + ), + ), + PopupMenuItem( + value: 'image', + child: ListTile( + leading: CircleAvatar( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + child: Icon(Icons.image_outlined), + ), + title: Text( + L10n.of(context).sendImage), + contentPadding: EdgeInsets.all(0), + ), + ), + if (PlatformInfos.isMobile) + PopupMenuItem( + value: 'camera', + child: ListTile( + leading: CircleAvatar( + backgroundColor: Colors.purple, + foregroundColor: Colors.white, + child: Icon( + Icons.camera_alt_outlined), + ), + title: Text( + L10n.of(context).openCamera), + contentPadding: EdgeInsets.all(0), + ), + ), + if (PlatformInfos.isMobile) + PopupMenuItem( + value: 'voice', + child: ListTile( + leading: CircleAvatar( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + child: Icon( + Icons.mic_none_outlined), + ), + title: Text(L10n.of(context) + .voiceMessage), + contentPadding: EdgeInsets.all(0), + ), + ), + ], + ), + ), + Container( + height: 56, + alignment: Alignment.center, + child: EncryptionButton(controller.room), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 4.0), + child: InputBar( + room: controller.room, + minLines: 1, + maxLines: kIsWeb ? 1 : 8, + autofocus: !PlatformInfos.isMobile, + keyboardType: !PlatformInfos.isMobile + ? TextInputType.text + : TextInputType.multiline, + onSubmitted: + controller.onInputBarSubmitted, + focusNode: controller.inputFocus, + controller: controller.sendController, + decoration: InputDecoration( + hintText: + L10n.of(context).writeAMessage, + hintMaxLines: 1, + border: InputBorder.none, + enabledBorder: InputBorder.none, + filled: false, + ), + onChanged: controller.onInputBarChanged, + ), + ), + ), + if (PlatformInfos.isMobile && + controller.inputText.isEmpty) + Container( + height: 56, + alignment: Alignment.center, + child: IconButton( + tooltip: L10n.of(context).voiceMessage, + icon: Icon(Icons.mic_none_outlined), + onPressed: + controller.voiceMessageAction, + ), + ), + if (!PlatformInfos.isMobile || + controller.inputText.isNotEmpty) + Container( + height: 56, + alignment: Alignment.center, + child: IconButton( + icon: Icon(Icons.send_outlined), + onPressed: controller.send, + tooltip: L10n.of(context).send, + ), + ), + ], + ), + ), + AnimatedContainer( + duration: Duration(milliseconds: 300), + height: controller.showEmojiPicker + ? MediaQuery.of(context).size.height / 2 + : 0, + child: controller.showEmojiPicker + ? EmojiPicker( + onEmojiSelected: controller.onEmojiSelected, + onBackspacePressed: controller.cancelEmojiPicker, + ) + : null, + ), + ], + ), + ), + ], + ), + )); } }