From c8e1aef74b7cf03e9949a636767f0f4728d80048 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Thu, 22 Oct 2026 10:25:50 -0400 Subject: [PATCH] show mutual rooms in user popover Fixes #97 --- lib/controllers/client.dart | 6 +++ lib/controllers/mutual_rooms.dart | 15 ++++++ lib/widgets/mutual_rooms_sheet.dart | 72 ++++++++++++++++++++++++++++ lib/widgets/read_receipts_sheet.dart | 1 - lib/widgets/user_bottom_sheet.dart | 25 ++++++++-- 5 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 lib/controllers/mutual_rooms.dart create mode 100644 lib/widgets/mutual_rooms_sheet.dart diff --git a/lib/controllers/client.dart b/lib/controllers/client.dart index 9a6cf67..ede7e11 100644 --- a/lib/controllers/client.dart +++ b/lib/controllers/client.dart @@ -371,6 +371,12 @@ class ClientController extends AsyncNotifier { Future exchangeToken(OAuthExchangeTokenRequest request) async => await _sendCommand("oauth_exchange_token", request.toJson()); + Future> getMutualRooms(String userId) async => IList( + ((await _sendCommand("get_mutual_rooms", {"user_id": userId}))["joined"] + as List?) + ?.cast(), + ); + Future getSpecVersions() async => .fromJson(await _sendCommand("get_versions")); diff --git a/lib/controllers/mutual_rooms.dart b/lib/controllers/mutual_rooms.dart new file mode 100644 index 0000000..da4086a --- /dev/null +++ b/lib/controllers/mutual_rooms.dart @@ -0,0 +1,15 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:nexus/controllers/client.dart"; + +class MutualRoomsController(final String userId) + extends AsyncNotifier> { + @override + Future> build() => + ref.read(ClientController.provider.notifier).getMutualRooms(userId); + + static final provider = AsyncNotifierProvider.family + .autoDispose, String>( + MutualRoomsController.new, + ); +} diff --git a/lib/widgets/mutual_rooms_sheet.dart b/lib/widgets/mutual_rooms_sheet.dart new file mode 100644 index 0000000..68b90e5 --- /dev/null +++ b/lib/widgets/mutual_rooms_sheet.dart @@ -0,0 +1,72 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:material_ui/material_ui.dart"; +import "package:nexus/controllers/mutual_rooms.dart"; +import "package:nexus/controllers/rooms.dart"; +import "package:nexus/controllers/sidebar_page.dart"; +import "package:nexus/helpers/extensions/better_when.dart"; +import "package:nexus/helpers/extensions/focus_room.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/widgets/sheet_layout.dart"; + +/// The rooms the user shares with [userId]. +class const MutualRoomsSheet(final String userId, {super.key}) + extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) => SheetLayout( + title: Text("Mutual Rooms"), + actions: [ + TextButton(onPressed: Navigator.of(context).pop, child: Text("Close")), + ], + children: [ + ref + .watch(MutualRoomsController.provider(userId)) + .betterWhen( + data: (roomIds) { + final rooms = ref.watch( + RoomsController.provider.select( + (allRooms) => roomIds + .map((roomId) => allRooms[roomId]?.metadata) + .nonNulls + .toIList(), + ), + ); + + return rooms.isEmpty + ? Text("No mutual rooms") + : Column( + children: rooms + .map( + (room) => ListTile( + leading: AvatarOrHash( + room.avatar, + room.name ?? "Unnamed Room", + fallback: Icon(Icons.numbers), + ), + title: Text( + room.name ?? "Unnamed Room", + overflow: .ellipsis, + ), + onTap: () async { + final container = ProviderScope.containerOf( + context, + ); + // Closes the user popover under this too + Navigator.of(context) + .popUntil((route) => route.isFirst); + await container.focusRoom(room.id); + container + .read( + SidebarPageController.provider.notifier, + ) + .set(.rooms); + }, + ), + ) + .toList(), + ); + }, + ), + ], + ); +} diff --git a/lib/widgets/read_receipts_sheet.dart b/lib/widgets/read_receipts_sheet.dart index 60793c8..aabdfb3 100644 --- a/lib/widgets/read_receipts_sheet.dart +++ b/lib/widgets/read_receipts_sheet.dart @@ -38,7 +38,6 @@ class const ReadReceiptsSheet( final name = member?.displayName ?? receipt.userId.localpart; return ListTile( - contentPadding: .zero, leading: AvatarOrHash(member?.avatarUrl, name, dimension: 40), title: Text(name, overflow: .ellipsis), subtitle: Text(receipt.userId, overflow: .ellipsis), diff --git a/lib/widgets/user_bottom_sheet.dart b/lib/widgets/user_bottom_sheet.dart index a5bb741..bcab7fc 100644 --- a/lib/widgets/user_bottom_sheet.dart +++ b/lib/widgets/user_bottom_sheet.dart @@ -8,6 +8,7 @@ import "package:intl/intl.dart"; import "package:m3e_buttons/m3e_buttons.dart"; import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/client_state.dart"; +import "package:nexus/controllers/mutual_rooms.dart"; import "package:nexus/controllers/power_level.dart"; import "package:nexus/controllers/profile.dart"; import "package:nexus/controllers/rooms.dart"; @@ -18,6 +19,7 @@ import "package:nexus/helpers/extensions/show_adaptive_sheet.dart"; import "package:nexus/helpers/extensions/get_localpart.dart"; import "package:nexus/widgets/error_dialog.dart"; import "package:nexus/widgets/loading.dart"; +import "package:nexus/widgets/mutual_rooms_sheet.dart"; import "package:nexus/models/membership_action.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/main.dart"; @@ -141,9 +143,6 @@ final class const UserBottomSheet( loading: () => Text(""), data: (profileResponse) => Column( children: [ - if (profileResponse.profile.timezone == null && - profileResponse.profile.pronouns.isEmpty) - Text(""), Wrap( crossAxisAlignment: .center, alignment: .center, @@ -195,6 +194,26 @@ final class const UserBottomSheet( ), ), + if (userId != ref.watch(ClientStateController.provider)?.userId) + switch (ref.watch(MutualRoomsController.provider(userId))) { + AsyncData(value: final roomIds) => switch (ref.watch( + RoomsController.provider.select( + (rooms) => roomIds.where(rooms.containsKey).length, + ), + )) { + 0 => SizedBox.shrink(), + final count => TextButton( + onPressed: () => context.showAdaptiveSheet( + (_) => MutualRoomsSheet(userId), + ), + child: Text( + "$count mutual ${Intl.plural(count, one: "room", other: "rooms")}", + ), + ), + }, + _ => SizedBox.shrink(), + }, + SizedBox(height: 8), if (userId != ref.watch(ClientStateController.provider)?.userId) ...[