From 493b7000c658892484bbf064220c339a588d05d2 Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Fri, 2 Oct 2020 15:50:59 +0200 Subject: [PATCH] feat: Enhance roomlist context menu --- lib/components/list_items/chat_list_item.dart | 274 +++++++----------- lib/components/theme_switcher.dart | 26 +- lib/views/chat_list.dart | 205 +++++++++---- 3 files changed, 268 insertions(+), 237 deletions(-) diff --git a/lib/components/list_items/chat_list_item.dart b/lib/components/list_items/chat_list_item.dart index e95e1099..2350a193 100644 --- a/lib/components/list_items/chat_list_item.dart +++ b/lib/components/list_items/chat_list_item.dart @@ -1,7 +1,6 @@ import 'package:famedlysdk/famedlysdk.dart'; import 'package:fluffychat/views/chat.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:pedantic/pedantic.dart'; @@ -18,11 +17,20 @@ import '../dialogs/send_file_dialog.dart'; class ChatListItem extends StatelessWidget { final Room room; final bool activeChat; + final bool selected; final Function onForget; + final Function onTap; + final Function onLongPress; - const ChatListItem(this.room, {this.activeChat = false, this.onForget}); + const ChatListItem(this.room, + {this.activeChat = false, + this.selected = false, + this.onTap, + this.onLongPress, + this.onForget}); void clickAction(BuildContext context) async { + if (onTap != null) return onTap(); if (!activeChat) { if (room.membership == Membership.invite && await SimpleDialogs(context) @@ -94,19 +102,7 @@ class ChatListItem extends StatelessWidget { } } - Future _toggleFavouriteRoom(BuildContext context) => - SimpleDialogs(context).tryRequestWithLoadingDialog( - room.setFavourite(!room.isFavourite), - ); - - Future _toggleMuted(BuildContext context) => - SimpleDialogs(context).tryRequestWithLoadingDialog( - room.setPushRuleState(room.pushRuleState == PushRuleState.notify - ? PushRuleState.mentions_only - : PushRuleState.notify), - ); - - Future archiveAction(BuildContext context) async { + Future archiveAction(BuildContext context) async { { if ([Membership.leave, Membership.ban].contains(room.membership)) { final success = await SimpleDialogs(context) @@ -117,163 +113,115 @@ class ChatListItem extends StatelessWidget { return success; } final confirmed = await SimpleDialogs(context).askConfirmation(); - if (!confirmed) { - return false; - } - final success = await SimpleDialogs(context) - .tryRequestWithLoadingDialog(room.leave()); - if (success == false) { - return false; - } - return true; + if (!confirmed) return; + await SimpleDialogs(context).tryRequestWithLoadingDialog(room.leave()); + return; } } @override Widget build(BuildContext context) { final isMuted = room.pushRuleState != PushRuleState.notify; - final slideableKey = GlobalKey(); - return Slidable( - key: slideableKey, - secondaryActions: [ - if ([Membership.join, Membership.invite].contains(room.membership)) - IconSlideAction( - caption: isMuted - ? L10n.of(context).unmuteChat - : L10n.of(context).muteChat, - color: Colors.blueGrey, - icon: - isMuted ? Icons.notifications_active : Icons.notifications_off, - onTap: () => _toggleMuted(context), - ), - if ([Membership.join, Membership.invite].contains(room.membership)) - IconSlideAction( - caption: room.isFavourite - ? L10n.of(context).unpin - : L10n.of(context).pin, - color: Colors.blue, - icon: room.isFavourite ? Icons.favorite_border : Icons.favorite, - onTap: () => _toggleFavouriteRoom(context), - ), - if ([Membership.join, Membership.invite].contains(room.membership)) - IconSlideAction( - caption: L10n.of(context).leave, - color: Colors.red, - icon: Icons.archive, - onTap: () => archiveAction(context), - ), - if ([Membership.leave, Membership.ban].contains(room.membership)) - IconSlideAction( - caption: L10n.of(context).delete, - color: Colors.red, - icon: Icons.delete_forever, - onTap: () => archiveAction(context), - ), - ], - actionPane: SlidableDrawerActionPane(), - child: Center( - child: Material( - color: chatListItemColor(context, activeChat), - child: ListTile( - onLongPress: () => (slideableKey.currentState as SlidableState) - .open(actionType: SlideActionType.secondary), - leading: Avatar(room.avatar, room.displayname), - title: Row( - children: [ - Expanded( - child: Text( - room.getLocalizedDisplayname(L10n.of(context)), - maxLines: 1, - overflow: TextOverflow.ellipsis, - softWrap: false, + return Center( + child: Material( + color: chatListItemColor(context, activeChat, selected), + child: ListTile( + onLongPress: onLongPress, + leading: Avatar(room.avatar, room.displayname), + title: Row( + children: [ + Expanded( + child: Text( + room.getLocalizedDisplayname(L10n.of(context)), + maxLines: 1, + overflow: TextOverflow.ellipsis, + softWrap: false, + ), + ), + room.isFavourite + ? Padding( + padding: const EdgeInsets.only(left: 4.0), + child: Icon( + Icons.favorite, + color: Colors.grey[400], + size: 16, + ), + ) + : Container(), + isMuted + ? Padding( + padding: const EdgeInsets.only(left: 4.0), + child: Icon( + Icons.notifications_off, + color: Colors.grey[400], + size: 16, + ), + ) + : Container(), + Padding( + padding: const EdgeInsets.only(left: 4.0), + child: Text( + room.timeCreated.localizedTimeShort(context), + style: TextStyle( + color: Color(0xFF555555), + fontSize: 13, ), ), - room.isFavourite - ? Padding( - padding: const EdgeInsets.only(left: 4.0), - child: Icon( - Icons.favorite, - color: Colors.grey[400], - size: 16, - ), - ) - : Container(), - isMuted - ? Padding( - padding: const EdgeInsets.only(left: 4.0), - child: Icon( - Icons.notifications_off, - color: Colors.grey[400], - size: 16, - ), - ) - : Container(), - Padding( - padding: const EdgeInsets.only(left: 4.0), - child: Text( - room.timeCreated.localizedTimeShort(context), - style: TextStyle( - color: Color(0xFF555555), - fontSize: 13, - ), - ), - ), - ], - ), - subtitle: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: room.membership == Membership.invite - ? Text( - L10n.of(context).youAreInvitedToThisChat, - style: TextStyle( - color: Theme.of(context).primaryColor, - ), - softWrap: false, - ) - : Text( - room.lastEvent?.getLocalizedBody( - L10n.of(context), - withSenderNamePrefix: !room.isDirectChat || - room.lastEvent.senderId == - room.client.userID, - hideReply: true, - ) ?? - '', - softWrap: false, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - decoration: room.lastEvent?.redacted == true - ? TextDecoration.lineThrough - : null, - ), - ), - ), - SizedBox(width: 8), - room.notificationCount > 0 - ? Container( - padding: EdgeInsets.symmetric(horizontal: 5), - height: 20, - decoration: BoxDecoration( - color: room.highlightCount > 0 - ? Colors.red - : Theme.of(context).primaryColor, - borderRadius: BorderRadius.circular(20), - ), - child: Center( - child: Text( - room.notificationCount.toString(), - style: TextStyle(color: Colors.white), - ), - ), - ) - : Text(' '), - ], - ), - onTap: () => clickAction(context), + ), + ], ), + subtitle: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: room.membership == Membership.invite + ? Text( + L10n.of(context).youAreInvitedToThisChat, + style: TextStyle( + color: Theme.of(context).primaryColor, + ), + softWrap: false, + ) + : Text( + room.lastEvent?.getLocalizedBody( + L10n.of(context), + withSenderNamePrefix: !room.isDirectChat || + room.lastEvent.senderId == room.client.userID, + hideReply: true, + ) ?? + '', + softWrap: false, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + decoration: room.lastEvent?.redacted == true + ? TextDecoration.lineThrough + : null, + ), + ), + ), + SizedBox(width: 8), + room.notificationCount > 0 + ? Container( + padding: EdgeInsets.symmetric(horizontal: 5), + height: 20, + decoration: BoxDecoration( + color: room.highlightCount > 0 + ? Colors.red + : Theme.of(context).primaryColor, + borderRadius: BorderRadius.circular(20), + ), + child: Center( + child: Text( + room.notificationCount.toString(), + style: TextStyle(color: Colors.white), + ), + ), + ) + : Text(' '), + ], + ), + onTap: () => clickAction(context), ), ), ); diff --git a/lib/components/theme_switcher.dart b/lib/components/theme_switcher.dart index aae674d3..892bd5a0 100644 --- a/lib/components/theme_switcher.dart +++ b/lib/components/theme_switcher.dart @@ -112,18 +112,20 @@ final ThemeData amoledTheme = ThemeData.dark().copyWith( ), ); -Color chatListItemColor(BuildContext context, bool activeChat) => - Theme.of(context).brightness == Brightness.light - ? activeChat - ? Color(0xFFE8E8E8) - : Colors.white - : activeChat - ? ThemeSwitcherWidget.of(context).amoledEnabled - ? Color(0xff121212) - : Colors.black - : ThemeSwitcherWidget.of(context).amoledEnabled - ? Colors.black - : Color(0xff121212); +Color chatListItemColor(BuildContext context, bool activeChat, bool selected) => + selected + ? Theme.of(context).primaryColor.withAlpha(50) + : Theme.of(context).brightness == Brightness.light + ? activeChat + ? Color(0xFFE8E8E8) + : Colors.white + : activeChat + ? ThemeSwitcherWidget.of(context).amoledEnabled + ? Color(0xff121212) + : Colors.black + : ThemeSwitcherWidget.of(context).amoledEnabled + ? Colors.black + : Color(0xff121212); Color blackWhiteColor(BuildContext context) => Theme.of(context).brightness == Brightness.light diff --git a/lib/views/chat_list.dart b/lib/views/chat_list.dart index 15387b17..bf66f4a0 100644 --- a/lib/views/chat_list.dart +++ b/lib/views/chat_list.dart @@ -25,7 +25,7 @@ import 'new_group.dart'; import 'new_private_chat.dart'; import 'settings.dart'; -enum SelectMode { normal, share } +enum SelectMode { normal, share, select } class ChatListView extends StatelessWidget { @override @@ -59,9 +59,15 @@ class _ChatListState extends State { PublicRoomsResponse publicRoomsResponse; bool loadingPublicRooms = false; String searchServer; + final _selectedRoomIds = {}; final ScrollController _scrollController = ScrollController(); + void _toggleSelection(String roomId) => + setState(() => _selectedRoomIds.contains(roomId) + ? _selectedRoomIds.remove(roomId) + : _selectedRoomIds.add(roomId)); + Future waitForFirstSync(BuildContext context) async { var client = Matrix.of(context).client; if (client.prevBatch?.isEmpty ?? true) { @@ -215,6 +221,39 @@ class _ChatListState extends State { super.dispose(); } + Future _toggleFavouriteRoom(BuildContext context) { + final room = Matrix.of(context).client.getRoomById(_selectedRoomIds.single); + return SimpleDialogs(context).tryRequestWithLoadingDialog( + room.setFavourite(!room.isFavourite), + ); + } + + Future _toggleMuted(BuildContext context) { + final room = Matrix.of(context).client.getRoomById(_selectedRoomIds.single); + return SimpleDialogs(context).tryRequestWithLoadingDialog( + room.setPushRuleState(room.pushRuleState == PushRuleState.notify + ? PushRuleState.mentions_only + : PushRuleState.notify), + ); + } + + Future _archiveAction(BuildContext context) async { + final confirmed = await SimpleDialogs(context).askConfirmation(); + if (!confirmed) return; + await SimpleDialogs(context) + .tryRequestWithLoadingDialog(_archiveSelectedRooms(context)); + setState(() => null); + } + + Future _archiveSelectedRooms(BuildContext context) async { + final client = Matrix.of(context).client; + while (_selectedRoomIds.isNotEmpty) { + final roomId = _selectedRoomIds.first; + await client.getRoomById(roomId).leave(); + _selectedRoomIds.remove(roomId); + } + } + @override Widget build(BuildContext context) { return StreamBuilder( @@ -232,10 +271,15 @@ class _ChatListState extends State { stream: Matrix.of(context).onShareContentChanged.stream, builder: (context, snapshot) { final selectMode = Matrix.of(context).shareContent == null - ? SelectMode.normal + ? _selectedRoomIds.isEmpty + ? SelectMode.normal + : SelectMode.select : SelectMode.share; + if (selectMode == SelectMode.share) { + _selectedRoomIds.clear(); + } return Scaffold( - drawer: selectMode == SelectMode.share + drawer: selectMode != SelectMode.normal ? null : Drawer( child: SafeArea( @@ -290,54 +334,81 @@ class _ChatListState extends State { ), ), appBar: AppBar( + centerTitle: false, elevation: _scrolledToTop ? 0 : null, - leading: selectMode != SelectMode.share - ? null - : IconButton( + leading: selectMode == SelectMode.share + ? IconButton( icon: Icon(Icons.close), onPressed: () => Matrix.of(context).shareContent = null, - ), + ) + : selectMode == SelectMode.select + ? IconButton( + icon: Icon(Icons.close), + onPressed: () => + setState(_selectedRoomIds.clear), + ) + : null, titleSpacing: 0, + actions: selectMode != SelectMode.select + ? null + : [ + if (_selectedRoomIds.length == 1) + IconButton( + icon: Icon(Icons.favorite_border_outlined), + onPressed: () => _toggleFavouriteRoom(context), + ), + if (_selectedRoomIds.length == 1) + IconButton( + icon: Icon(Icons.notifications_none), + onPressed: () => _toggleMuted(context), + ), + IconButton( + icon: Icon(Icons.archive), + onPressed: () => _archiveAction(context), + ), + ], title: selectMode == SelectMode.share ? Text(L10n.of(context).share) - : Container( - height: 40, - padding: EdgeInsets.only(right: 8), - child: Material( - color: Theme.of(context).secondaryHeaderColor, - borderRadius: BorderRadius.circular(32), - child: TextField( - autocorrect: false, - controller: searchController, - focusNode: _searchFocusNode, - decoration: InputDecoration( - contentPadding: EdgeInsets.only( - top: 8, - bottom: 8, - left: 16, + : selectMode == SelectMode.select + ? Text(_selectedRoomIds.length.toString()) + : Container( + height: 40, + padding: EdgeInsets.only(right: 8), + child: Material( + color: Theme.of(context).secondaryHeaderColor, + borderRadius: BorderRadius.circular(32), + child: TextField( + autocorrect: false, + controller: searchController, + focusNode: _searchFocusNode, + decoration: InputDecoration( + contentPadding: EdgeInsets.only( + top: 8, + bottom: 8, + left: 16, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(32), + ), + hintText: L10n.of(context).searchForAChat, + suffixIcon: searchMode + ? IconButton( + icon: Icon(Icons.backspace), + onPressed: () => setState(() { + searchController.clear(); + _searchFocusNode.unfocus(); + }), + ) + : null, + ), ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(32), - ), - hintText: L10n.of(context).searchForAChat, - suffixIcon: searchMode - ? IconButton( - icon: Icon(Icons.backspace), - onPressed: () => setState(() { - searchController.clear(); - _searchFocusNode.unfocus(); - }), - ) - : null, ), ), - ), - ), ), floatingActionButton: (AdaptivePageLayout.columnMode(context) || - selectMode == SelectMode.share) + selectMode != SelectMode.normal) ? null : FloatingActionButton( child: Icon(Icons.add), @@ -436,34 +507,32 @@ class _ChatListState extends State { itemBuilder: (BuildContext context, int i) { if (i == 0) { + final displayPresences = directChats + .isNotEmpty && + selectMode == SelectMode.normal; return Column( mainAxisSize: MainAxisSize.min, children: [ - (directChats.isEmpty || - selectMode == - SelectMode.share) - ? Container() - : PreferredSize( - preferredSize: - Size.fromHeight(82), - child: Container( - height: 78, - child: - ListView.builder( - scrollDirection: - Axis.horizontal, - itemCount: - directChats - .length, - itemBuilder: (BuildContext - context, - int i) => - PresenceListItem( - directChats[ - i]), - ), + AnimatedContainer( + duration: Duration( + milliseconds: 500), + height: + displayPresences ? 78 : 0, + child: !displayPresences + ? null + : ListView.builder( + scrollDirection: + Axis.horizontal, + itemCount: directChats + .length, + itemBuilder: (BuildContext + context, + int i) => + PresenceListItem( + directChats[ + i]), ), - ), + ), ], ); } @@ -471,6 +540,18 @@ class _ChatListState extends State { return i < rooms.length ? ChatListItem( rooms[i], + selected: _selectedRoomIds + .contains(rooms[i].id), + onTap: selectMode == + SelectMode.select + ? () => _toggleSelection( + rooms[i].id) + : null, + onLongPress: selectMode != + SelectMode.share + ? () => _toggleSelection( + rooms[i].id) + : null, activeChat: widget.activeChat == rooms[i].id,