show mutual rooms in user popover

Fixes #97
This commit is contained in:
Henry Hiles 2026-10-22 10:25:50 -04:00
commit c8e1aef74b
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
5 changed files with 115 additions and 4 deletions

View file

@ -371,6 +371,12 @@ class ClientController extends AsyncNotifier<int> {
Future<void> exchangeToken(OAuthExchangeTokenRequest request) async =>
await _sendCommand("oauth_exchange_token", request.toJson());
Future<IList<String>> getMutualRooms(String userId) async => IList(
((await _sendCommand("get_mutual_rooms", {"user_id": userId}))["joined"]
as List?)
?.cast<String>(),
);
Future<SpecVersionsResponse> getSpecVersions() async =>
.fromJson(await _sendCommand("get_versions"));

View file

@ -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<IList<String>> {
@override
Future<IList<String>> build() =>
ref.read(ClientController.provider.notifier).getMutualRooms(userId);
static final provider = AsyncNotifierProvider.family
.autoDispose<MutualRoomsController, IList<String>, String>(
MutualRoomsController.new,
);
}

View file

@ -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(),
);
},
),
],
);
}

View file

@ -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),

View file

@ -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) ...[