diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 904b06c9c..6c6da498a 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2710,5 +2710,6 @@ "@goToSpace": { "type": "text", "space": {} - } + }, + "markAsUnread": "Mark as unread" } diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 7084a082d..6c8ebc755 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -619,7 +619,7 @@ class ChatListController extends State if (space != null) SheetAction( key: ChatContextAction.goToSpace, - icon: Icons.workspaces_outlined, + icon: Icons.chevron_right_outlined, label: L10n.of(context)!.goToSpace(space.getLocalizedDisplayname()), ), SheetAction( @@ -627,11 +627,13 @@ class ChatListController extends State icon: room.markedUnread ? Icons.mark_as_unread : Icons.mark_as_unread_outlined, - label: L10n.of(context)!.toggleUnread, + label: room.markedUnread + ? L10n.of(context)!.markAsRead + : L10n.of(context)!.markAsUnread, ), SheetAction( key: ChatContextAction.favorite, - icon: room.isFavourite ? Icons.pin : Icons.pin_outlined, + icon: room.isFavourite ? Icons.push_pin : Icons.push_pin_outlined, label: room.isFavourite ? L10n.of(context)!.unpin : L10n.of(context)!.pin, @@ -640,7 +642,7 @@ class ChatListController extends State key: ChatContextAction.mute, icon: room.pushRuleState == PushRuleState.notify ? Icons.notifications_off_outlined - : Icons.notifications, + : Icons.notifications_outlined, label: room.pushRuleState == PushRuleState.notify ? L10n.of(context)!.muteChat : L10n.of(context)!.unmuteChat, @@ -657,6 +659,19 @@ class ChatListController extends State if (action == null) return; if (!mounted) return; + if (action == ChatContextAction.leave) { + final confirmed = await showOkCancelAlertDialog( + useRootNavigator: false, + context: context, + title: L10n.of(context)!.areYouSure, + okLabel: L10n.of(context)!.yes, + cancelLabel: L10n.of(context)!.no, + message: L10n.of(context)!.archiveRoomDescription, + ); + if (confirmed == OkCancelResult.cancel) return; + } + if (!mounted) return; + await showFutureLoadingDialog( context: context, future: () async { diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index 81f765125..93fa8575b 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -22,14 +22,9 @@ class ChatListView extends StatelessWidget { final selectMode = controller.selectMode; return PopScope( canPop: controller.selectMode == SelectMode.normal && - !controller.isSearchMode && - controller.activeFilter == ActiveFilter.allChats, - onPopInvoked: (pop) async { + !controller.isSearchMode, + onPopInvoked: (pop) { if (pop) return; - if (controller.activeSpaceId != null) { - controller.clearActiveSpace(); - return; - } final selMode = controller.selectMode; if (controller.isSearchMode) { controller.cancelSearch(); diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index 6459de1e8..8fe443bf1 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -165,316 +165,312 @@ class _SpaceViewState extends State { final room = Matrix.of(context).client.getRoomById(widget.spaceId); final displayname = room?.getLocalizedDisplayname() ?? L10n.of(context)!.nothingFound; - return Scaffold( - appBar: AppBar( - leading: Center( - child: CloseButton( - onPressed: widget.onBack, - ), - ), - titleSpacing: 0, - title: ListTile( - contentPadding: EdgeInsets.zero, - leading: Avatar( - mxContent: room?.avatar, - name: displayname, - borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2), + return PopScope( + canPop: false, + onPopInvoked: (_) => widget.onBack(), + child: Scaffold( + appBar: AppBar( + leading: Center( + child: CloseButton( + onPressed: widget.onBack, + ), ), - title: Text( - displayname, - maxLines: 1, - overflow: TextOverflow.ellipsis, + titleSpacing: 0, + title: ListTile( + contentPadding: EdgeInsets.zero, + leading: Avatar( + mxContent: room?.avatar, + name: displayname, + borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2), + ), + title: Text( + displayname, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: room == null + ? null + : Text( + L10n.of(context)!.countChatsAndCountParticipants( + room.spaceChildren.length, + room.summary.mJoinedMemberCount ?? 1, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), - subtitle: room == null - ? null - : Text( - L10n.of(context)!.countChatsAndCountParticipants( - room.spaceChildren.length, - room.summary.mJoinedMemberCount ?? 1, + actions: [ + PopupMenuButton( + onSelected: _onSpaceAction, + itemBuilder: (context) => [ + PopupMenuItem( + value: SpaceActions.settings, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.settings_outlined), + const SizedBox(width: 12), + Text(L10n.of(context)!.settings), + ], ), - maxLines: 1, - overflow: TextOverflow.ellipsis, ), - ), - actions: [ - PopupMenuButton( - onSelected: _onSpaceAction, - itemBuilder: (context) => [ - PopupMenuItem( - value: SpaceActions.settings, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.settings_outlined), - const SizedBox(width: 12), - Text(L10n.of(context)!.settings), - ], + PopupMenuItem( + value: SpaceActions.invite, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.person_add_outlined), + const SizedBox(width: 12), + Text(L10n.of(context)!.invite), + ], + ), ), - ), - PopupMenuItem( - value: SpaceActions.invite, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.person_add_outlined), - const SizedBox(width: 12), - Text(L10n.of(context)!.invite), - ], + PopupMenuItem( + value: SpaceActions.leave, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.delete_outlined), + const SizedBox(width: 12), + Text(L10n.of(context)!.leave), + ], + ), ), - ), - PopupMenuItem( - value: SpaceActions.leave, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.delete_outlined), - const SizedBox(width: 12), - Text(L10n.of(context)!.leave), - ], + ], + ), + ], + ), + body: room == null + ? const Center( + child: Icon( + Icons.search_outlined, + size: 80, ), - ), - ], - ), - ], - ), - body: room == null - ? const Center( - child: Icon( - Icons.search_outlined, - size: 80, - ), - ) - : StreamBuilder( - stream: room.client.onSync.stream - .where((s) => s.hasRoomUpdate) - .rateLimit(const Duration(seconds: 1)), - builder: (context, snapshot) { - final joinedRooms = room.spaceChildren - .map((child) { - final roomId = child.roomId; - if (roomId == null) return null; - return room.client.getRoomById(roomId); - }) - .whereType() - .where((room) => room.membership != Membership.leave) - .toList(); + ) + : StreamBuilder( + stream: room.client.onSync.stream + .where((s) => s.hasRoomUpdate) + .rateLimit(const Duration(seconds: 1)), + builder: (context, snapshot) { + final childrenIds = room.spaceChildren + .map((c) => c.roomId) + .whereType() + .toSet(); - // Sort rooms by last activity - joinedRooms.sort( - (b, a) => (a.lastEvent?.originServerTs ?? - DateTime.fromMillisecondsSinceEpoch(0)) - .compareTo( - b.lastEvent?.originServerTs ?? - DateTime.fromMillisecondsSinceEpoch(0), - ), - ); + final joinedRooms = room.client.rooms + .where((room) => childrenIds.remove(room.id)) + .toList(); - final joinedParents = room.spaceParents - .map((parent) { - final roomId = parent.roomId; - if (roomId == null) return null; - return room.client.getRoomById(roomId); - }) - .whereType() - .toList(); - final filter = _filterController.text.trim().toLowerCase(); - return CustomScrollView( - slivers: [ - SliverAppBar( - floating: true, - toolbarHeight: 72, - scrolledUnderElevation: 0, - backgroundColor: Colors.transparent, - automaticallyImplyLeading: false, - title: TextField( - controller: _filterController, - onChanged: (_) => setState(() {}), - textInputAction: TextInputAction.search, - decoration: InputDecoration( - fillColor: - Theme.of(context).colorScheme.secondaryContainer, - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(99), - ), - contentPadding: EdgeInsets.zero, - hintText: L10n.of(context)!.search, - hintStyle: TextStyle( - color: Theme.of(context) + final joinedParents = room.spaceParents + .map((parent) { + final roomId = parent.roomId; + if (roomId == null) return null; + return room.client.getRoomById(roomId); + }) + .whereType() + .toList(); + final filter = _filterController.text.trim().toLowerCase(); + return CustomScrollView( + slivers: [ + SliverAppBar( + floating: true, + toolbarHeight: 72, + scrolledUnderElevation: 0, + backgroundColor: Colors.transparent, + automaticallyImplyLeading: false, + title: TextField( + controller: _filterController, + onChanged: (_) => setState(() {}), + textInputAction: TextInputAction.search, + decoration: InputDecoration( + fillColor: Theme.of(context) .colorScheme - .onPrimaryContainer, - fontWeight: FontWeight.normal, - ), - floatingLabelBehavior: FloatingLabelBehavior.never, - prefixIcon: IconButton( - onPressed: () {}, - icon: Icon( - Icons.search_outlined, + .secondaryContainer, + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(99), + ), + contentPadding: EdgeInsets.zero, + hintText: L10n.of(context)!.search, + hintStyle: TextStyle( color: Theme.of(context) .colorScheme .onPrimaryContainer, + fontWeight: FontWeight.normal, + ), + floatingLabelBehavior: FloatingLabelBehavior.never, + prefixIcon: IconButton( + onPressed: () {}, + icon: Icon( + Icons.search_outlined, + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), ), ), ), ), - ), - SliverList.builder( - itemCount: joinedParents.length, - itemBuilder: (context, i) { - final displayname = - joinedParents[i].getLocalizedDisplayname(); - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 1, - ), - child: Material( - borderRadius: - BorderRadius.circular(AppConfig.borderRadius), - clipBehavior: Clip.hardEdge, - child: ListTile( - minVerticalPadding: 0, - leading: Icon( - Icons.adaptive.arrow_back_outlined, - size: 16, - ), - title: Row( - children: [ - Avatar( - mxContent: joinedParents[i].avatar, - name: displayname, - size: Avatar.defaultSize / 2, - borderRadius: BorderRadius.circular( - AppConfig.borderRadius / 4, + SliverList.builder( + itemCount: joinedParents.length, + itemBuilder: (context, i) { + final displayname = + joinedParents[i].getLocalizedDisplayname(); + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 1, + ), + child: Material( + borderRadius: + BorderRadius.circular(AppConfig.borderRadius), + clipBehavior: Clip.hardEdge, + child: ListTile( + minVerticalPadding: 0, + leading: Icon( + Icons.adaptive.arrow_back_outlined, + size: 16, + ), + title: Row( + children: [ + Avatar( + mxContent: joinedParents[i].avatar, + name: displayname, + size: Avatar.defaultSize / 2, + borderRadius: BorderRadius.circular( + AppConfig.borderRadius / 4, + ), ), - ), - const SizedBox(width: 8), - Expanded(child: Text(displayname)), - ], + const SizedBox(width: 8), + Expanded(child: Text(displayname)), + ], + ), + onTap: () => + widget.toParentSpace(joinedParents[i].id), ), - onTap: () => - widget.toParentSpace(joinedParents[i].id), ), - ), - ); - }, - ), - SliverList.builder( - itemCount: joinedRooms.length + 1, - itemBuilder: (context, i) { - if (i == 0) { - return SearchTitle( - title: L10n.of(context)!.joinedChats, - icon: const Icon(Icons.chat_outlined), ); - } - i--; - final room = joinedRooms[i]; - return ChatListItem( - room, - filter: filter, - onTap: () => widget.onChatTab(room), - onLongPress: () => widget.onChatContext(room), - activeChat: widget.activeChat == room.id, - ); - }, - ), - SliverList.builder( - itemCount: _discoveredChildren.length + 2, - itemBuilder: (context, i) { - if (i == 0) { - return SearchTitle( - title: L10n.of(context)!.discover, - icon: const Icon(Icons.explore_outlined), + }, + ), + SliverList.builder( + itemCount: joinedRooms.length + 1, + itemBuilder: (context, i) { + if (i == 0) { + return SearchTitle( + title: L10n.of(context)!.joinedChats, + icon: const Icon(Icons.chat_outlined), + ); + } + i--; + final room = joinedRooms[i]; + return ChatListItem( + room, + filter: filter, + onTap: () => widget.onChatTab(room), + onLongPress: () => widget.onChatContext(room), + activeChat: widget.activeChat == room.id, ); - } - i--; - if (i == _discoveredChildren.length) { - if (_noMoreRooms) { - return Padding( - padding: const EdgeInsets.all(12.0), - child: Center( - child: Text( - L10n.of(context)!.noMoreChatsFound, - style: const TextStyle(fontSize: 13), + }, + ), + SliverList.builder( + itemCount: _discoveredChildren.length + 2, + itemBuilder: (context, i) { + if (i == 0) { + return SearchTitle( + title: L10n.of(context)!.discover, + icon: const Icon(Icons.explore_outlined), + ); + } + i--; + if (i == _discoveredChildren.length) { + if (_noMoreRooms) { + return Padding( + padding: const EdgeInsets.all(12.0), + child: Center( + child: Text( + L10n.of(context)!.noMoreChatsFound, + style: const TextStyle(fontSize: 13), + ), ), + ); + } + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 2.0, + ), + child: TextButton( + onPressed: _isLoading ? null : _loadHierarchy, + child: _isLoading + ? LinearProgressIndicator( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + ) + : Text(L10n.of(context)!.loadMore), ), ); } + final item = _discoveredChildren[i]; + final displayname = item.name ?? + item.canonicalAlias ?? + L10n.of(context)!.emptyChat; + if (!displayname.toLowerCase().contains(filter)) { + return const SizedBox.shrink(); + } return Padding( padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 2.0, + horizontal: 8, + vertical: 1, ), - child: TextButton( - onPressed: _isLoading ? null : _loadHierarchy, - child: _isLoading - ? LinearProgressIndicator( - borderRadius: BorderRadius.circular( - AppConfig.borderRadius, + child: Material( + borderRadius: + BorderRadius.circular(AppConfig.borderRadius), + clipBehavior: Clip.hardEdge, + child: ListTile( + onTap: () => _joinChildRoom(item), + leading: Avatar( + mxContent: item.avatarUrl, + name: displayname, + borderRadius: item.roomType == 'm.space' + ? BorderRadius.circular( + AppConfig.borderRadius / 2, + ) + : null, + ), + title: Row( + children: [ + Expanded( + child: Text( + displayname, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - ) - : Text(L10n.of(context)!.loadMore), - ), - ); - } - final item = _discoveredChildren[i]; - final displayname = item.name ?? - item.canonicalAlias ?? - L10n.of(context)!.emptyChat; - if (!displayname.toLowerCase().contains(filter)) { - return const SizedBox.shrink(); - } - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 1, - ), - child: Material( - borderRadius: - BorderRadius.circular(AppConfig.borderRadius), - clipBehavior: Clip.hardEdge, - child: ListTile( - onTap: () => _joinChildRoom(item), - leading: Avatar( - mxContent: item.avatarUrl, - name: displayname, - borderRadius: item.roomType == 'm.space' - ? BorderRadius.circular( - AppConfig.borderRadius / 2, - ) - : null, - ), - title: Row( - children: [ - Expanded( - child: Text( - displayname, - maxLines: 1, - overflow: TextOverflow.ellipsis, ), - ), - const SizedBox(width: 8), - const Icon(Icons.add_circle_outline_outlined), - ], - ), - subtitle: Text( - item.topic ?? - L10n.of(context)!.countParticipants( - item.numJoinedMembers, + const SizedBox(width: 8), + const Icon( + Icons.add_circle_outline_outlined, ), - maxLines: 1, - overflow: TextOverflow.ellipsis, + ], + ), + subtitle: Text( + item.topic ?? + L10n.of(context)!.countParticipants( + item.numJoinedMembers, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), ), - ), - ); - }, - ), - ], - ); - }, - ), + ); + }, + ), + ], + ); + }, + ), + ), ); } }