add room directory support
This commit is contained in:
parent
033c7f9c05
commit
e382499a2a
18 changed files with 868 additions and 245 deletions
|
|
@ -36,6 +36,7 @@ import "package:nexus/models/requests/get_related_events.dart";
|
||||||
import "package:nexus/models/requests/get_room_state.dart";
|
import "package:nexus/models/requests/get_room_state.dart";
|
||||||
import "package:nexus/models/requests/get_specific_room_state.dart";
|
import "package:nexus/models/requests/get_specific_room_state.dart";
|
||||||
import "package:nexus/models/requests/get_space_hierarchy.dart";
|
import "package:nexus/models/requests/get_space_hierarchy.dart";
|
||||||
|
import "package:nexus/models/requests/get_room_directory.dart";
|
||||||
import "package:nexus/models/requests/join_room.dart";
|
import "package:nexus/models/requests/join_room.dart";
|
||||||
import "package:nexus/models/profile_response.dart";
|
import "package:nexus/models/profile_response.dart";
|
||||||
import "package:nexus/models/push_notification.dart";
|
import "package:nexus/models/push_notification.dart";
|
||||||
|
|
@ -57,6 +58,7 @@ import "package:nexus/models/requests/set_state.dart";
|
||||||
import "package:nexus/models/requests/update_push_rule.dart";
|
import "package:nexus/models/requests/update_push_rule.dart";
|
||||||
import "package:nexus/models/requests/upload_media.dart";
|
import "package:nexus/models/requests/upload_media.dart";
|
||||||
import "package:nexus/models/room.dart";
|
import "package:nexus/models/room.dart";
|
||||||
|
import "package:nexus/models/room_directory.dart";
|
||||||
import "package:nexus/models/room_summary.dart";
|
import "package:nexus/models/room_summary.dart";
|
||||||
import "package:nexus/models/space_hierarchy.dart";
|
import "package:nexus/models/space_hierarchy.dart";
|
||||||
import "package:nexus/models/spec_versions_response.dart";
|
import "package:nexus/models/spec_versions_response.dart";
|
||||||
|
|
@ -311,6 +313,11 @@ class ClientController(final String? accountId) extends AsyncNotifier<int> {
|
||||||
) async =>
|
) async =>
|
||||||
.fromJson(await _sendCommand("get_space_hierarchy", request.toJson()));
|
.fromJson(await _sendCommand("get_space_hierarchy", request.toJson()));
|
||||||
|
|
||||||
|
Future<RoomDirectory> getRoomDirectory(
|
||||||
|
GetRoomDirectoryRequest request,
|
||||||
|
) async =>
|
||||||
|
.fromJson(await _sendCommand("get_room_directory", request.toJson()));
|
||||||
|
|
||||||
Future<RoomSummary> getRoomSummary(JoinRoomRequest request) async =>
|
Future<RoomSummary> getRoomSummary(JoinRoomRequest request) async =>
|
||||||
.fromJson(await _sendCommand("get_room_summary", request.toJson()));
|
.fromJson(await _sendCommand("get_room_summary", request.toJson()));
|
||||||
|
|
||||||
|
|
|
||||||
39
lib/controllers/room_directory.dart
Normal file
39
lib/controllers/room_directory.dart
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import "package:flutter_riverpod/experimental/mutation.dart";
|
||||||
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/client.dart";
|
||||||
|
import "package:nexus/models/room_directory.dart";
|
||||||
|
|
||||||
|
/// The public room directory of a server, or of the user's own if null.
|
||||||
|
class RoomDirectoryController(final String? server)
|
||||||
|
extends AsyncNotifier<RoomDirectory> {
|
||||||
|
@override
|
||||||
|
Future<RoomDirectory> build() => ref
|
||||||
|
.read(ClientController.provider.notifier)
|
||||||
|
.getRoomDirectory(.new(server: server));
|
||||||
|
|
||||||
|
Future<void> loadMore() async {
|
||||||
|
final current = await future;
|
||||||
|
if (current.nextBatch == null) return;
|
||||||
|
|
||||||
|
final next = await ref
|
||||||
|
.read(ClientController.provider.notifier)
|
||||||
|
.getRoomDirectory(.new(server: server, since: current.nextBatch));
|
||||||
|
|
||||||
|
if (!ref.mounted) return;
|
||||||
|
|
||||||
|
state = AsyncData(
|
||||||
|
current.copyWith(
|
||||||
|
rooms: current.rooms.addAll(next.rooms),
|
||||||
|
nextBatch: next.nextBatch,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loading the next page of a directory, keyed by its server
|
||||||
|
static final loadMoreMutation = Mutation<void>();
|
||||||
|
|
||||||
|
static final provider = AsyncNotifierProvider.family
|
||||||
|
.autoDispose<RoomDirectoryController, RoomDirectory, String?>(
|
||||||
|
RoomDirectoryController.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,12 @@ extension GetLocalpart on String {
|
||||||
/// Whether this looks like a valid Matrix user ID, e.g. `@user:example.com`.
|
/// Whether this looks like a valid Matrix user ID, e.g. `@user:example.com`.
|
||||||
bool get isValidUserId => RegExp(r"^@[^:\s]+:[^\s]+$").hasMatch(this);
|
bool get isValidUserId => RegExp(r"^@[^:\s]+:[^\s]+$").hasMatch(this);
|
||||||
|
|
||||||
|
/// Whether this is a valid Matrix server name, e.g. `example.com:8448`.
|
||||||
|
bool get isValidServerName => RegExp(
|
||||||
|
r"^(\[[0-9a-f:.]+\]|[a-z0-9.-]{1,255})(:\d{1,5})?$",
|
||||||
|
caseSensitive: false,
|
||||||
|
).hasMatch(this);
|
||||||
|
|
||||||
String? get serverName {
|
String? get serverName {
|
||||||
final index = indexOf(":");
|
final index = indexOf(":");
|
||||||
return index == -1 ? null : substring(index + 1);
|
return index == -1 ? null : substring(index + 1);
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ extension JoinRoom on BuildContext {
|
||||||
}) async {
|
}) async {
|
||||||
final container = ProviderScope.containerOf(this);
|
final container = ProviderScope.containerOf(this);
|
||||||
final messenger = ScaffoldMessenger.of(this);
|
final messenger = ScaffoldMessenger.of(this);
|
||||||
|
final navigator = Navigator.of(this);
|
||||||
|
|
||||||
final roomId = await withProgressSnackbar(
|
final roomId = await withProgressSnackbar(
|
||||||
"Joining $name...",
|
"Joining $name...",
|
||||||
|
|
@ -31,6 +32,7 @@ extension JoinRoom on BuildContext {
|
||||||
action: .new(
|
action: .new(
|
||||||
label: "Open",
|
label: "Open",
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
navigator.popUntil((route) => route.isFirst);
|
||||||
await container.focusRoom(roomId);
|
await container.focusRoom(roomId);
|
||||||
container.read(SidebarPageController.provider.notifier).set(.rooms);
|
container.read(SidebarPageController.provider.notifier).set(.rooms);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
18
lib/helpers/extensions/room_summary_display.dart
Normal file
18
lib/helpers/extensions/room_summary_display.dart
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import "package:material_ui/material_ui.dart";
|
||||||
|
import "package:nexus/models/content/create.dart";
|
||||||
|
import "package:nexus/models/room_summary.dart";
|
||||||
|
|
||||||
|
extension RoomSummaryDisplay on RoomSummary {
|
||||||
|
bool get isSpace => roomType == RoomType.space;
|
||||||
|
|
||||||
|
IconData get icon => isSpace ? Icons.workspaces : Icons.numbers;
|
||||||
|
|
||||||
|
String get displayName =>
|
||||||
|
name ?? canonicalAlias ?? (isSpace ? "Unnamed Space" : "Unnamed Room");
|
||||||
|
|
||||||
|
/// E.g. "Space · 12 members"
|
||||||
|
String get details => [
|
||||||
|
if (isSpace) "Space",
|
||||||
|
if (joinedMembers case final members?) "$members members",
|
||||||
|
].join(" · ");
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ import "package:collection/collection.dart";
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:freezed_annotation/freezed_annotation.dart";
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
import "package:nexus/helpers/extensions/get_link_info.dart";
|
import "package:nexus/helpers/extensions/get_link_info.dart";
|
||||||
|
import "package:nexus/helpers/extensions/get_localpart.dart";
|
||||||
import "package:nexus/models/content/content.dart";
|
import "package:nexus/models/content/content.dart";
|
||||||
|
|
||||||
part "bot_command_description.freezed.dart";
|
part "bot_command_description.freezed.dart";
|
||||||
|
|
@ -181,10 +182,6 @@ enum BotPrimitiveType(final String label, final String expected) {
|
||||||
@JsonValue("event_id")
|
@JsonValue("event_id")
|
||||||
eventId("event", r"a message link or event ID like $abc");
|
eventId("event", r"a message link or event ID like $abc");
|
||||||
|
|
||||||
static final serverNamePattern = RegExp(
|
|
||||||
r"^(\[[0-9A-Fa-f:.]+\]|[A-Za-z0-9.-]+)(:\d{1,5})?$",
|
|
||||||
);
|
|
||||||
|
|
||||||
Object? parse(String text, {required String roomId}) {
|
Object? parse(String text, {required String roomId}) {
|
||||||
final (:identifier, :eventId, :via) =
|
final (:identifier, :eventId, :via) =
|
||||||
text.linkInfo ??
|
text.linkInfo ??
|
||||||
|
|
@ -206,7 +203,7 @@ enum BotPrimitiveType(final String label, final String expected) {
|
||||||
identifier,
|
identifier,
|
||||||
.roomAlias when identifier.startsWith("#") && identifier.contains(":") =>
|
.roomAlias when identifier.startsWith("#") && identifier.contains(":") =>
|
||||||
identifier,
|
identifier,
|
||||||
.serverName when serverNamePattern.hasMatch(text) => text,
|
.serverName when text.isValidServerName => text,
|
||||||
.roomId when identifier.startsWith("!") && eventId == null => {
|
.roomId when identifier.startsWith("!") && eventId == null => {
|
||||||
"type": "room_id",
|
"type": "room_id",
|
||||||
"id": identifier,
|
"id": identifier,
|
||||||
|
|
|
||||||
28
lib/models/requests/get_room_directory.dart
Normal file
28
lib/models/requests/get_room_directory.dart
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
|
|
||||||
|
part "get_room_directory.freezed.dart";
|
||||||
|
part "get_room_directory.g.dart";
|
||||||
|
|
||||||
|
@Freezed(toJson: false, fromJson: false)
|
||||||
|
@JsonSerializable(includeIfNull: false, explicitToJson: true)
|
||||||
|
class const GetRoomDirectoryRequest({
|
||||||
|
final String? server,
|
||||||
|
final int limit = 48,
|
||||||
|
final String? since,
|
||||||
|
final PublicRoomsFilter? filter,
|
||||||
|
}) with _$GetRoomDirectoryRequest {
|
||||||
|
Map<String, Object?> toJson() => _$GetRoomDirectoryRequestToJson(this);
|
||||||
|
|
||||||
|
factory GetRoomDirectoryRequest.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$GetRoomDirectoryRequestFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Freezed(toJson: false, fromJson: false)
|
||||||
|
@JsonSerializable(includeIfNull: false)
|
||||||
|
class const PublicRoomsFilter({final String? genericSearchTerm})
|
||||||
|
with _$PublicRoomsFilter {
|
||||||
|
Map<String, Object?> toJson() => _$PublicRoomsFilterToJson(this);
|
||||||
|
|
||||||
|
factory PublicRoomsFilter.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$PublicRoomsFilterFromJson(json);
|
||||||
|
}
|
||||||
17
lib/models/room_directory.dart
Normal file
17
lib/models/room_directory.dart
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
|
import "package:nexus/models/room_summary.dart";
|
||||||
|
|
||||||
|
part "room_directory.freezed.dart";
|
||||||
|
part "room_directory.g.dart";
|
||||||
|
|
||||||
|
@Freezed(toJson: false, fromJson: false)
|
||||||
|
@JsonSerializable(createToJson: false)
|
||||||
|
class const RoomDirectory({
|
||||||
|
@JsonKey(name: "chunk") final IList<RoomSummary> rooms = const .empty(),
|
||||||
|
final String? nextBatch,
|
||||||
|
final int? totalRoomCountEstimate,
|
||||||
|
}) with _$RoomDirectory {
|
||||||
|
factory RoomDirectory.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$RoomDirectoryFromJson(json);
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:material_ui/material_ui.dart";
|
import "package:material_ui/material_ui.dart";
|
||||||
import "package:freezed_annotation/freezed_annotation.dart";
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
import "package:nexus/models/name_color.dart";
|
import "package:nexus/models/name_color.dart";
|
||||||
|
|
@ -25,6 +26,10 @@ class const Settings({
|
||||||
/// When not sending, read receipts are still sent privately, so that the
|
/// When not sending, read receipts are still sent privately, so that the
|
||||||
/// user's other devices know what has been read.
|
/// user's other devices know what has been read.
|
||||||
final ShowAndSend readReceipts = .showAndSend,
|
final ShowAndSend readReceipts = .showAndSend,
|
||||||
|
final IList<String> directoryServers = const IListConst([
|
||||||
|
"matrix.org",
|
||||||
|
"matrixrooms.info",
|
||||||
|
]),
|
||||||
}) with _$Settings {
|
}) with _$Settings {
|
||||||
Map<String, Object?> toJson() => _$SettingsToJson(this);
|
Map<String, Object?> toJson() => _$SettingsToJson(this);
|
||||||
|
|
||||||
|
|
|
||||||
54
lib/widgets/add_directory_server_sheet.dart
Normal file
54
lib/widgets/add_directory_server_sheet.dart
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
|
import "package:material_ui/material_ui.dart";
|
||||||
|
import "package:material_adaptive_sheets/material_adaptive_sheets.dart";
|
||||||
|
import "package:nexus/helpers/extensions/get_localpart.dart";
|
||||||
|
|
||||||
|
class const AddDirectoryServerSheet({
|
||||||
|
required final ISet<String> existing,
|
||||||
|
super.key,
|
||||||
|
}) extends HookWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final controller = useTextEditingController();
|
||||||
|
final text = useValueListenable(controller).text;
|
||||||
|
final server = text.trim().toLowerCase();
|
||||||
|
final error = server.isEmpty
|
||||||
|
? null
|
||||||
|
: !server.isValidServerName
|
||||||
|
? "Enter a server name, without https:// or a path"
|
||||||
|
: existing.contains(server)
|
||||||
|
? "This server has already been added"
|
||||||
|
: null;
|
||||||
|
final valid = server.isNotEmpty && error == null;
|
||||||
|
|
||||||
|
void submit() {
|
||||||
|
if (valid) Navigator.of(context).pop(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SheetLayout(
|
||||||
|
title: const Text("Add Server"),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: Navigator.of(context).pop,
|
||||||
|
child: const Text("Cancel"),
|
||||||
|
),
|
||||||
|
TextButton(onPressed: valid ? submit : null, child: const Text("Add")),
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
const Text("Enter a server to browse the public rooms of."),
|
||||||
|
TextField(
|
||||||
|
controller: controller,
|
||||||
|
autofocus: true,
|
||||||
|
keyboardType: .url,
|
||||||
|
onSubmitted: (_) => submit(),
|
||||||
|
decoration: .new(
|
||||||
|
labelText: "Server name",
|
||||||
|
hintText: "matrix.org",
|
||||||
|
errorText: error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,8 +3,8 @@ import "package:material_ui/material_ui.dart";
|
||||||
import "package:material_adaptive_sheets/material_adaptive_sheets.dart";
|
import "package:material_adaptive_sheets/material_adaptive_sheets.dart";
|
||||||
import "package:nexus/controllers/invites.dart";
|
import "package:nexus/controllers/invites.dart";
|
||||||
import "package:nexus/helpers/extensions/get_localpart.dart";
|
import "package:nexus/helpers/extensions/get_localpart.dart";
|
||||||
|
import "package:nexus/helpers/extensions/room_summary_display.dart";
|
||||||
import "package:nexus/main.dart";
|
import "package:nexus/main.dart";
|
||||||
import "package:nexus/models/content/create.dart";
|
|
||||||
import "package:nexus/models/nav_page.dart";
|
import "package:nexus/models/nav_page.dart";
|
||||||
import "package:nexus/models/room_summary.dart";
|
import "package:nexus/models/room_summary.dart";
|
||||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||||
|
|
@ -120,13 +120,7 @@ class const InviteCard(final RoomSummary invite, {super.key})
|
||||||
invite.avatarUrl,
|
invite.avatarUrl,
|
||||||
name,
|
name,
|
||||||
dimension: 40,
|
dimension: 40,
|
||||||
fallback: invite.isDirect
|
fallback: invite.isDirect ? null : Icon(invite.icon),
|
||||||
? null
|
|
||||||
: Icon(
|
|
||||||
invite.roomType == RoomType.space
|
|
||||||
? Icons.workspaces
|
|
||||||
: Icons.numbers,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
title: Text(name, maxLines: 1, overflow: .ellipsis),
|
title: Text(name, maxLines: 1, overflow: .ellipsis),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
|
|
|
||||||
36
lib/widgets/m3e_rail_theme.dart
Normal file
36
lib/widgets/m3e_rail_theme.dart
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import "package:material_ui/material_ui.dart";
|
||||||
|
import "package:m3e_design/m3e_design.dart";
|
||||||
|
|
||||||
|
// Needed for navigation_rail_m3e (#65).
|
||||||
|
import "package:flutter/material.dart" as old_mat;
|
||||||
|
|
||||||
|
/// Themes a navigation_rail_m3e rail, which still uses the old Material theme.
|
||||||
|
class const M3ERailTheme({
|
||||||
|
required final WidgetBuilder builder,
|
||||||
|
final List<old_mat.ThemeExtension> extensions = const [],
|
||||||
|
super.key,
|
||||||
|
}) extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => MaterialUiCompatibilityBridge(
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final theme = old_mat.Theme.of(context);
|
||||||
|
return old_mat.Theme(
|
||||||
|
data: theme.copyWith(
|
||||||
|
extensions: [
|
||||||
|
...extensions,
|
||||||
|
// Otherwise the FAB label falls back to Roboto
|
||||||
|
M3ETheme.defaults(theme.colorScheme).copyWith(
|
||||||
|
typography: .defaultFor(
|
||||||
|
theme.brightness,
|
||||||
|
baseOverride: theme.textTheme,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Builder(builder: builder),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
338
lib/widgets/pages/explore.dart
Normal file
338
lib/widgets/pages/explore.dart
Normal file
|
|
@ -0,0 +1,338 @@
|
||||||
|
import "dart:math";
|
||||||
|
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
|
import "package:material_ui/material_ui.dart";
|
||||||
|
import "package:material_adaptive_sheets/material_adaptive_sheets.dart";
|
||||||
|
import "package:navigation_rail_m3e/navigation_rail_m3e.dart";
|
||||||
|
import "package:nexus/controllers/client.dart";
|
||||||
|
import "package:nexus/controllers/client_state.dart";
|
||||||
|
import "package:nexus/controllers/room_directory.dart";
|
||||||
|
import "package:nexus/controllers/rooms.dart";
|
||||||
|
import "package:nexus/controllers/settings.dart";
|
||||||
|
import "package:nexus/helpers/extensions/get_localpart.dart";
|
||||||
|
import "package:nexus/main.dart";
|
||||||
|
import "package:nexus/helpers/extensions/room_summary_display.dart";
|
||||||
|
import "package:nexus/widgets/add_directory_server_sheet.dart";
|
||||||
|
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||||
|
import "package:nexus/widgets/room_summary_sheet.dart";
|
||||||
|
import "package:nexus/widgets/divider_text.dart";
|
||||||
|
import "package:nexus/widgets/error_dialog.dart";
|
||||||
|
import "package:nexus/widgets/loading.dart";
|
||||||
|
import "package:nexus/widgets/m3e_rail_theme.dart";
|
||||||
|
import "package:nexus/widgets/room_directory_card.dart";
|
||||||
|
|
||||||
|
class const ExplorePage({super.key}) extends HookConsumerWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final ownServer = ref.watch(
|
||||||
|
ClientStateController.provider.select(
|
||||||
|
(state) => state?.userId?.serverName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final savedServers = ref
|
||||||
|
.watch(
|
||||||
|
SettingsController.provider.select(
|
||||||
|
(settings) => settings.value?.directoryServers,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
?.where((server) => server != ownServer)
|
||||||
|
.toIList();
|
||||||
|
|
||||||
|
// Null is the user's own server
|
||||||
|
final selected = useState<String?>(null);
|
||||||
|
final servers = IList<String?>([null, ...?savedServers]);
|
||||||
|
String labelOf(String? server) => server ?? ownServer ?? "Home";
|
||||||
|
IconData iconOf(String? server) => server == null ? Icons.home : Icons.dns;
|
||||||
|
|
||||||
|
final current = servers[max(0, servers.indexOf(selected.value))];
|
||||||
|
final provider = RoomDirectoryController.provider(current);
|
||||||
|
final loadMore = RoomDirectoryController.loadMoreMutation(current);
|
||||||
|
final loadingMore = ref.watch(loadMore).isPending;
|
||||||
|
final directory = ref.watch(provider);
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
final via = IList([?current ?? ownServer]);
|
||||||
|
|
||||||
|
Future<void> addServer() async {
|
||||||
|
final server = await context.showAdaptiveSheet<String>(
|
||||||
|
(_) => AddDirectoryServerSheet(
|
||||||
|
existing: .new([?ownServer, ...servers.nonNulls]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (server == null) return;
|
||||||
|
|
||||||
|
final settings = await ref.read(SettingsController.provider.future);
|
||||||
|
await ref
|
||||||
|
.read(SettingsController.provider.notifier)
|
||||||
|
.set(
|
||||||
|
settings.copyWith(
|
||||||
|
directoryServers: settings.directoryServers.add(server),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (context.mounted) selected.value = server;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeServer(String server) async {
|
||||||
|
selected.value = null;
|
||||||
|
final settings = await ref.read(SettingsController.provider.future);
|
||||||
|
await ref
|
||||||
|
.read(SettingsController.provider.notifier)
|
||||||
|
.set(
|
||||||
|
settings.copyWith(
|
||||||
|
directoryServers: settings.directoryServers.remove(server),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final isWide = constraints.maxWidth >= 700;
|
||||||
|
|
||||||
|
final searchBar = SearchAnchor.bar(
|
||||||
|
barHintText: "Search...",
|
||||||
|
isFullScreen: !isWide,
|
||||||
|
suggestionsBuilder: (suggestionsContext, controller) async {
|
||||||
|
final text = controller.text.trim();
|
||||||
|
if (text.isEmpty) return const [];
|
||||||
|
|
||||||
|
// Each search hits the server, so wait for the user to stop typing
|
||||||
|
await Future.delayed(Durations.long2);
|
||||||
|
if (controller.text.trim() != text) return const [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
final directory = await ref
|
||||||
|
.read(ClientController.provider.notifier)
|
||||||
|
.getRoomDirectory(
|
||||||
|
.new(
|
||||||
|
server: current,
|
||||||
|
limit: 20,
|
||||||
|
filter: .new(genericSearchTerm: text),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return directory.rooms.map(
|
||||||
|
(summary) => Consumer(
|
||||||
|
builder: (_, ref, _) {
|
||||||
|
final joined = ref.watch(
|
||||||
|
RoomsController.provider.select(
|
||||||
|
(rooms) => rooms.containsKey(summary.roomId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(suggestionsContext).pop();
|
||||||
|
controller.text = "";
|
||||||
|
|
||||||
|
context.showAdaptiveSheet(
|
||||||
|
(_) => RoomSummarySheet(
|
||||||
|
summary,
|
||||||
|
via: joined ? null : via,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
leading: AvatarOrHash(
|
||||||
|
summary.avatarUrl,
|
||||||
|
summary.displayName,
|
||||||
|
fallback: Icon(summary.icon),
|
||||||
|
),
|
||||||
|
title: Text(summary.displayName),
|
||||||
|
subtitle: Text(
|
||||||
|
[
|
||||||
|
summary.details,
|
||||||
|
if (joined) "Joined",
|
||||||
|
].where((part) => part.isNotEmpty).join(" · "),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
return [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.error_outline),
|
||||||
|
title: const Text("Couldn't search this server"),
|
||||||
|
subtitle: Text(error.toString()),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final results = CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverPadding(
|
||||||
|
padding: const .all(16),
|
||||||
|
sliver: SliverList.list(
|
||||||
|
children: [
|
||||||
|
if (!isWide)
|
||||||
|
Padding(
|
||||||
|
padding: const .only(bottom: 12),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: .horizontal,
|
||||||
|
child: Row(
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
...servers.map(
|
||||||
|
(server) => ChoiceChip(
|
||||||
|
avatar: Icon(iconOf(server)),
|
||||||
|
label: Text(labelOf(server)),
|
||||||
|
selected: current == server,
|
||||||
|
onSelected: (_) => selected.value = server,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ActionChip(
|
||||||
|
avatar: const Icon(Icons.add),
|
||||||
|
label: const Text("Add server"),
|
||||||
|
onPressed: () => addServer().onError(showError),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
searchBar,
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(switch (directory.value?.totalRoomCountEstimate) {
|
||||||
|
final count? when count > 0 => "Popular Rooms ($count)",
|
||||||
|
_ => "Popular Rooms",
|
||||||
|
}, style: theme.textTheme.titleLarge),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...switch (directory) {
|
||||||
|
AsyncData(:final value) when value.rooms.isEmpty => [
|
||||||
|
SliverFillRemaining(
|
||||||
|
hasScrollBody: false,
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: .min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.search_off,
|
||||||
|
size: 48,
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
"No rooms found",
|
||||||
|
style: theme.textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
AsyncData(:final value) || AsyncLoading(:final value?) => [
|
||||||
|
SliverPadding(
|
||||||
|
padding: const .symmetric(horizontal: 16),
|
||||||
|
sliver: SliverLayoutBuilder(
|
||||||
|
builder: (_, constraints) => SliverGrid.builder(
|
||||||
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: max(
|
||||||
|
1,
|
||||||
|
(constraints.crossAxisExtent + 12) ~/ (280 + 12),
|
||||||
|
),
|
||||||
|
mainAxisExtent: 210,
|
||||||
|
crossAxisSpacing: 12,
|
||||||
|
mainAxisSpacing: 12,
|
||||||
|
),
|
||||||
|
itemCount: value.rooms.length,
|
||||||
|
itemBuilder: (_, index) =>
|
||||||
|
RoomDirectoryCard(value.rooms[index], via: via),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverPadding(
|
||||||
|
padding: const .all(16),
|
||||||
|
sliver: SliverToBoxAdapter(
|
||||||
|
child: value.nextBatch == null
|
||||||
|
? null
|
||||||
|
: Center(
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: loadingMore
|
||||||
|
? null
|
||||||
|
: () => loadMore
|
||||||
|
.run(
|
||||||
|
ref,
|
||||||
|
(transaction) => transaction
|
||||||
|
.get(provider.notifier)
|
||||||
|
.loadMore(),
|
||||||
|
)
|
||||||
|
.onError(showError),
|
||||||
|
child: const Text("Load more"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
AsyncError(:final error, :final stackTrace) => [
|
||||||
|
SliverFillRemaining(
|
||||||
|
hasScrollBody: false,
|
||||||
|
child: ErrorDialog(error, stackTrace, provider: provider),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
AsyncLoading() => [
|
||||||
|
const SliverFillRemaining(
|
||||||
|
hasScrollBody: false,
|
||||||
|
child: Loading(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("Explore Rooms"),
|
||||||
|
actionsPadding: const .symmetric(horizontal: 12),
|
||||||
|
actions: [
|
||||||
|
if (current case final server?)
|
||||||
|
IconButton(
|
||||||
|
tooltip: "Remove server",
|
||||||
|
onPressed: () => removeServer(server).onError(showError),
|
||||||
|
icon: const Icon(Icons.delete_outline),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: isWide
|
||||||
|
? Row(
|
||||||
|
children: [
|
||||||
|
M3ERailTheme(
|
||||||
|
builder: (_) => NavigationRailM3E(
|
||||||
|
type: .alwaysExpand,
|
||||||
|
scrollable: true,
|
||||||
|
fab: .new(
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: "Add server",
|
||||||
|
tooltip: "Add a server to browse",
|
||||||
|
onPressed: () => addServer().onError(showError),
|
||||||
|
),
|
||||||
|
sections: [
|
||||||
|
.new(
|
||||||
|
header: const DividerText("Servers"),
|
||||||
|
destinations: servers
|
||||||
|
.map(
|
||||||
|
(server) => NavigationRailM3EDestination(
|
||||||
|
icon: Icon(iconOf(server)),
|
||||||
|
label: labelOf(server),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
selectedIndex: servers.indexOf(current),
|
||||||
|
onDestinationSelected: (index) =>
|
||||||
|
selected.value = servers[index],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const VerticalDivider(),
|
||||||
|
Expanded(child: results),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: results,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
130
lib/widgets/room_directory_card.dart
Normal file
130
lib/widgets/room_directory_card.dart
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
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:material_adaptive_sheets/material_adaptive_sheets.dart";
|
||||||
|
import "package:nexus/controllers/rooms.dart";
|
||||||
|
import "package:nexus/controllers/sidebar_page.dart";
|
||||||
|
import "package:nexus/helpers/extensions/focus_room.dart";
|
||||||
|
import "package:nexus/helpers/extensions/join_room.dart";
|
||||||
|
import "package:nexus/helpers/extensions/room_summary_display.dart";
|
||||||
|
import "package:nexus/models/room_summary.dart";
|
||||||
|
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||||
|
import "package:nexus/widgets/knock_sheet.dart";
|
||||||
|
import "package:nexus/widgets/room_summary_sheet.dart";
|
||||||
|
|
||||||
|
class const RoomDirectoryCard(
|
||||||
|
final RoomSummary summary, {
|
||||||
|
required final IList<String> via,
|
||||||
|
super.key,
|
||||||
|
}) extends ConsumerWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final joined = ref.watch(
|
||||||
|
RoomsController.provider.select(
|
||||||
|
(rooms) => rooms.containsKey(summary.roomId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final mustKnock = ref.watch(
|
||||||
|
RoomsController.provider.select(
|
||||||
|
(rooms) => summary.mustKnock(
|
||||||
|
summary.allowedRoomIds.where(rooms.containsKey).toISet(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final name = summary.displayName;
|
||||||
|
|
||||||
|
return Card.filled(
|
||||||
|
clipBehavior: .antiAlias,
|
||||||
|
margin: .zero,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => context.showAdaptiveSheet(
|
||||||
|
(_) => RoomSummarySheet(summary, via: joined ? null : via),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const .all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: .stretch,
|
||||||
|
spacing: 12,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
spacing: 12,
|
||||||
|
children: [
|
||||||
|
AvatarOrHash(
|
||||||
|
summary.avatarUrl,
|
||||||
|
name,
|
||||||
|
dimension: 48,
|
||||||
|
fallback: Icon(summary.icon),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: .start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
name,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: .ellipsis,
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
if (summary.details case final details
|
||||||
|
when details.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
details,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: .ellipsis,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
summary.topic ?? summary.canonicalAlias ?? "",
|
||||||
|
maxLines: 3,
|
||||||
|
overflow: .ellipsis,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: .centerRight,
|
||||||
|
child: joined
|
||||||
|
? FilledButton.tonal(
|
||||||
|
onPressed: () async {
|
||||||
|
final container = ProviderScope.containerOf(context);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
await container.focusRoom(summary.roomId);
|
||||||
|
container
|
||||||
|
.read(SidebarPageController.provider.notifier)
|
||||||
|
.set(.rooms);
|
||||||
|
},
|
||||||
|
child: const Text("Open"),
|
||||||
|
)
|
||||||
|
: mustKnock
|
||||||
|
? FilledButton(
|
||||||
|
onPressed: () => context.showAdaptiveSheet(
|
||||||
|
(_) =>
|
||||||
|
KnockSheet(summary.roomId, name: name, via: via),
|
||||||
|
),
|
||||||
|
child: const Text("Knock"),
|
||||||
|
)
|
||||||
|
: FilledButton(
|
||||||
|
onPressed: () => context.joinRoom(
|
||||||
|
.new(roomIdOrAlias: summary.roomId, via: via),
|
||||||
|
name: name,
|
||||||
|
),
|
||||||
|
child: const Text("Join"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,6 @@ import "dart:math";
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
import "package:material_ui/material_ui.dart";
|
import "package:material_ui/material_ui.dart";
|
||||||
import "package:m3e_design/m3e_design.dart";
|
|
||||||
import "package:navigation_rail_m3e/navigation_rail_m3e.dart";
|
import "package:navigation_rail_m3e/navigation_rail_m3e.dart";
|
||||||
import "package:material_adaptive_sheets/material_adaptive_sheets.dart";
|
import "package:material_adaptive_sheets/material_adaptive_sheets.dart";
|
||||||
import "package:nexus/helpers/extensions/get_all_child_rooms.dart";
|
import "package:nexus/helpers/extensions/get_all_child_rooms.dart";
|
||||||
|
|
@ -17,8 +16,10 @@ import "package:nexus/widgets/create_room_sheet.dart";
|
||||||
import "package:nexus/widgets/start_dm_sheet.dart";
|
import "package:nexus/widgets/start_dm_sheet.dart";
|
||||||
import "package:nexus/widgets/divider_widget.dart";
|
import "package:nexus/widgets/divider_widget.dart";
|
||||||
import "package:nexus/widgets/error_dialog.dart";
|
import "package:nexus/widgets/error_dialog.dart";
|
||||||
|
import "package:nexus/widgets/pages/explore.dart";
|
||||||
import "package:nexus/widgets/pages/settings.dart";
|
import "package:nexus/widgets/pages/settings.dart";
|
||||||
import "package:nexus/widgets/join_sheet.dart";
|
import "package:nexus/widgets/join_sheet.dart";
|
||||||
|
import "package:nexus/widgets/m3e_rail_theme.dart";
|
||||||
import "package:nexus/widgets/room_summary_sheet.dart";
|
import "package:nexus/widgets/room_summary_sheet.dart";
|
||||||
import "package:nexus/widgets/room_menu_button.dart";
|
import "package:nexus/widgets/room_menu_button.dart";
|
||||||
import "package:nexus/widgets/space_hierarchy_sheet.dart";
|
import "package:nexus/widgets/space_hierarchy_sheet.dart";
|
||||||
|
|
@ -28,9 +29,6 @@ import "package:nexus/controllers/selected_space.dart";
|
||||||
import "package:nexus/controllers/rooms.dart";
|
import "package:nexus/controllers/rooms.dart";
|
||||||
import "package:nexus/controllers/spaces.dart";
|
import "package:nexus/controllers/spaces.dart";
|
||||||
|
|
||||||
// Needed for navigation_rail_m3e (#65).
|
|
||||||
import "package:flutter/material.dart" as old_mat;
|
|
||||||
|
|
||||||
class const RoomList({super.key}) extends ConsumerWidget implements NavPage {
|
class const RoomList({super.key}) extends ConsumerWidget implements NavPage {
|
||||||
@override
|
@override
|
||||||
String get title => "Rooms";
|
String get title => "Rooms";
|
||||||
|
|
@ -92,18 +90,11 @@ class const SpacesRail({super.key}) extends ConsumerWidget {
|
||||||
.select((async) => async.requireValue),
|
.select((async) => async.requireValue),
|
||||||
);
|
);
|
||||||
|
|
||||||
return MaterialUiCompatibilityBridge(
|
return M3ERailTheme(
|
||||||
child: Builder(
|
|
||||||
builder: (context) => old_mat.Theme(
|
|
||||||
data: old_mat.Theme.of(context).copyWith(
|
|
||||||
extensions: [
|
extensions: [
|
||||||
NavigationRailM3ETheme(
|
NavigationRailM3ETheme(itemCollapsedHeight: 48, itemVerticalGap: 0),
|
||||||
itemCollapsedHeight: 48,
|
|
||||||
itemVerticalGap: 0,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
builder: (context) => Container(
|
||||||
child: Container(
|
|
||||||
color: NavigationRailTokensAdapter(context).containerColor,
|
color: NavigationRailTokensAdapter(context).containerColor,
|
||||||
padding: .only(top: 16),
|
padding: .only(top: 16),
|
||||||
child: NavigationRailM3E(
|
child: NavigationRailM3E(
|
||||||
|
|
@ -112,14 +103,10 @@ class const SpacesRail({super.key}) extends ConsumerWidget {
|
||||||
scrollable: true,
|
scrollable: true,
|
||||||
onDestinationSelected: (value) {
|
onDestinationSelected: (value) {
|
||||||
ref
|
ref
|
||||||
.read(
|
.read(KeyController.provider(KeyController.spaceKey).notifier)
|
||||||
KeyController.provider(KeyController.spaceKey).notifier,
|
|
||||||
)
|
|
||||||
.set(spaces[value].id);
|
.set(spaces[value].id);
|
||||||
ref
|
ref
|
||||||
.read(
|
.read(KeyController.provider(KeyController.roomKey).notifier)
|
||||||
KeyController.provider(KeyController.roomKey).notifier,
|
|
||||||
)
|
|
||||||
.set(spaces[value].children.firstOrNull?.metadata?.id);
|
.set(spaces[value].children.firstOrNull?.metadata?.id);
|
||||||
},
|
},
|
||||||
sections: [
|
sections: [
|
||||||
|
|
@ -193,10 +180,12 @@ class const SpacesRail({super.key}) extends ConsumerWidget {
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
),
|
),
|
||||||
const AccountSwitcher(),
|
const AccountSwitcher(),
|
||||||
const IconButton(
|
IconButton(
|
||||||
tooltip: "Explore other rooms (Not yet implemented)",
|
tooltip: "Explore other rooms",
|
||||||
onPressed: null,
|
onPressed: () => Navigator.of(context).push(
|
||||||
icon: Icon(Icons.explore),
|
MaterialPageRoute(builder: (_) => const ExplorePage()),
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.explore),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: "Open settings",
|
tooltip: "Open settings",
|
||||||
|
|
@ -211,8 +200,6 @@ class const SpacesRail({super.key}) extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -254,28 +241,15 @@ class const RoomsPane({super.key}) extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
body: MaterialUiCompatibilityBridge(
|
body: M3ERailTheme(
|
||||||
child: Builder(
|
|
||||||
builder: (context) {
|
|
||||||
final theme = old_mat.Theme.of(context);
|
|
||||||
return old_mat.Theme(
|
|
||||||
data: theme.copyWith(
|
|
||||||
extensions: [
|
extensions: [
|
||||||
NavigationRailM3ETheme(
|
NavigationRailM3ETheme(
|
||||||
itemExpandedHeight: 48,
|
itemExpandedHeight: 48,
|
||||||
iconLabelGap: 16,
|
iconLabelGap: 16,
|
||||||
sectionHeaderSpacingTop: 0,
|
sectionHeaderSpacingTop: 0,
|
||||||
),
|
),
|
||||||
// Otherwise the FAB label falls back to Roboto
|
|
||||||
M3ETheme.defaults(theme.colorScheme).copyWith(
|
|
||||||
typography: .defaultFor(
|
|
||||||
theme.brightness,
|
|
||||||
baseOverride: theme.textTheme,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
builder: (context) => Padding(
|
||||||
child: Padding(
|
|
||||||
padding: const .only(top: 4),
|
padding: const .only(top: 4),
|
||||||
child: NavigationRailM3E(
|
child: NavigationRailM3E(
|
||||||
fab: selectedSpace.room == null
|
fab: selectedSpace.room == null
|
||||||
|
|
@ -309,9 +283,7 @@ class const RoomsPane({super.key}) extends ConsumerWidget {
|
||||||
header: selectedSpace.room == null
|
header: selectedSpace.room == null
|
||||||
? null
|
? null
|
||||||
: const DividerWidget(Text("Rooms")),
|
: const DividerWidget(Text("Rooms")),
|
||||||
destinations: selectedSpace.children.toDestinations(
|
destinations: selectedSpace.children.toDestinations(isDm: isDm),
|
||||||
isDm: isDm,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
...selectedSpace.subSpaces.map(
|
...selectedSpace.subSpaces.map(
|
||||||
(subSpace) => NavigationRailM3ESection(
|
(subSpace) => NavigationRailM3ESection(
|
||||||
|
|
@ -323,14 +295,12 @@ class const RoomsPane({super.key}) extends ConsumerWidget {
|
||||||
if (subSpace.room.metadata?.avatar != null)
|
if (subSpace.room.metadata?.avatar != null)
|
||||||
AvatarOrHash(
|
AvatarOrHash(
|
||||||
subSpace.room.metadata?.avatar,
|
subSpace.room.metadata?.avatar,
|
||||||
subSpace.room.metadata?.name ??
|
subSpace.room.metadata?.name ?? "Unnamed Room",
|
||||||
"Unnamed Room",
|
|
||||||
dimension: 16,
|
dimension: 16,
|
||||||
),
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
subSpace.room.metadata?.name ??
|
subSpace.room.metadata?.name ?? "Unnamed Space",
|
||||||
"Unnamed Space",
|
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: .ellipsis,
|
overflow: .ellipsis,
|
||||||
),
|
),
|
||||||
|
|
@ -338,18 +308,13 @@ class const RoomsPane({super.key}) extends ConsumerWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
destinations: subSpace.children.toDestinations(
|
destinations: subSpace.children.toDestinations(isDm: isDm),
|
||||||
isDm: isDm,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
onDestinationSelected: (value) {
|
onDestinationSelected: (value) {
|
||||||
ref
|
ref
|
||||||
.read(
|
.read(KeyController.provider(KeyController.roomKey).notifier)
|
||||||
KeyController.provider(KeyController.roomKey)
|
|
||||||
.notifier,
|
|
||||||
)
|
|
||||||
.set(destinationIds[value]);
|
.set(destinationIds[value]);
|
||||||
if (Navigator.of(context).canPop()) {
|
if (Navigator.of(context).canPop()) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
|
@ -357,9 +322,6 @@ class const RoomsPane({super.key}) extends ConsumerWidget {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import "package:nexus/controllers/invites.dart";
|
||||||
import "package:nexus/controllers/rooms.dart";
|
import "package:nexus/controllers/rooms.dart";
|
||||||
import "package:nexus/helpers/extensions/get_localpart.dart";
|
import "package:nexus/helpers/extensions/get_localpart.dart";
|
||||||
import "package:nexus/helpers/extensions/join_room.dart";
|
import "package:nexus/helpers/extensions/join_room.dart";
|
||||||
|
import "package:nexus/helpers/extensions/room_summary_display.dart";
|
||||||
import "package:nexus/main.dart";
|
import "package:nexus/main.dart";
|
||||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||||
import "package:nexus/widgets/expandable_image.dart";
|
import "package:nexus/widgets/expandable_image.dart";
|
||||||
|
|
@ -44,15 +45,11 @@ class const RoomSummarySheet(
|
||||||
summary.avatarUrl,
|
summary.avatarUrl,
|
||||||
"",
|
"",
|
||||||
dimension: 64,
|
dimension: 64,
|
||||||
fallback: Icon(Icons.numbers),
|
fallback: Icon(summary.icon),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(summary.displayName, overflow: .ellipsis, maxLines: 3),
|
||||||
summary.name ?? summary.canonicalAlias ?? "Unnamed Room",
|
|
||||||
overflow: .ellipsis,
|
|
||||||
maxLines: 3,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import "package:nexus/models/settings_category.dart";
|
||||||
import "package:nexus/widgets/pages/settings_category.dart";
|
import "package:nexus/widgets/pages/settings_category.dart";
|
||||||
import "package:nexus/widgets/divider_text.dart";
|
import "package:nexus/widgets/divider_text.dart";
|
||||||
import "package:nexus/widgets/highlight_wrapper.dart";
|
import "package:nexus/widgets/highlight_wrapper.dart";
|
||||||
|
import "package:nexus/widgets/m3e_rail_theme.dart";
|
||||||
import "package:super_sliver_list/super_sliver_list.dart";
|
import "package:super_sliver_list/super_sliver_list.dart";
|
||||||
|
|
||||||
/// A settings screen split into categories: a list of them that opens each
|
/// A settings screen split into categories: a list of them that opens each
|
||||||
|
|
@ -170,8 +171,8 @@ class const SettingsLayout({
|
||||||
)
|
)
|
||||||
: Row(
|
: Row(
|
||||||
children: [
|
children: [
|
||||||
MaterialUiCompatibilityBridge(
|
M3ERailTheme(
|
||||||
child: NavigationRailM3E(
|
builder: (_) => NavigationRailM3E(
|
||||||
type: .alwaysExpand,
|
type: .alwaysExpand,
|
||||||
trailing: searchBar,
|
trailing: searchBar,
|
||||||
scrollable: true,
|
scrollable: true,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import "package:nexus/controllers/rooms.dart";
|
||||||
import "package:nexus/controllers/space_hierarchy.dart";
|
import "package:nexus/controllers/space_hierarchy.dart";
|
||||||
import "package:nexus/helpers/extensions/better_when.dart";
|
import "package:nexus/helpers/extensions/better_when.dart";
|
||||||
import "package:nexus/helpers/extensions/join_room.dart";
|
import "package:nexus/helpers/extensions/join_room.dart";
|
||||||
|
import "package:nexus/helpers/extensions/room_summary_display.dart";
|
||||||
import "package:nexus/helpers/extensions/show_leave_room_sheet.dart";
|
import "package:nexus/helpers/extensions/show_leave_room_sheet.dart";
|
||||||
import "package:nexus/helpers/extensions/with_progress_snackbar.dart";
|
import "package:nexus/helpers/extensions/with_progress_snackbar.dart";
|
||||||
import "package:nexus/main.dart";
|
import "package:nexus/main.dart";
|
||||||
|
|
@ -163,10 +164,7 @@ class const SpaceHierarchySheet(final String spaceId, {super.key})
|
||||||
keyMapper: (room) => room.summary.roomId,
|
keyMapper: (room) => room.summary.roomId,
|
||||||
values: hierarchy.rooms,
|
values: hierarchy.rooms,
|
||||||
);
|
);
|
||||||
final spaceName =
|
final spaceName = byId[spaceId]?.summary.displayName ?? "Unnamed Space";
|
||||||
byId[spaceId]?.summary.name ??
|
|
||||||
byId[spaceId]?.summary.canonicalAlias ??
|
|
||||||
"Unnamed Space";
|
|
||||||
final children = (byId[spaceId]?.children ?? const .empty())
|
final children = (byId[spaceId]?.children ?? const .empty())
|
||||||
.map(
|
.map(
|
||||||
(child) => switch (byId[child.roomId]) {
|
(child) => switch (byId[child.roomId]) {
|
||||||
|
|
@ -197,16 +195,13 @@ class const SpaceHierarchySheet(final String spaceId, {super.key})
|
||||||
Builder(
|
Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final summary = room.summary;
|
final summary = room.summary;
|
||||||
final name =
|
final name = summary.displayName;
|
||||||
summary.name ??
|
|
||||||
summary.canonicalAlias ??
|
|
||||||
"Unnamed Room";
|
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: AvatarOrHash(
|
leading: AvatarOrHash(
|
||||||
summary.avatarUrl,
|
summary.avatarUrl,
|
||||||
name,
|
name,
|
||||||
fallback: const Icon(Icons.numbers),
|
fallback: Icon(summary.icon),
|
||||||
),
|
),
|
||||||
title: Text(name, maxLines: 1, overflow: .ellipsis),
|
title: Text(name, maxLines: 1, overflow: .ellipsis),
|
||||||
subtitle: summary.topic == null
|
subtitle: summary.topic == null
|
||||||
|
|
@ -239,10 +234,7 @@ class const SpaceHierarchySheet(final String spaceId, {super.key})
|
||||||
builder: (context, ref, _) {
|
builder: (context, ref, _) {
|
||||||
final expanded = useState(false);
|
final expanded = useState(false);
|
||||||
final summary = room.summary;
|
final summary = room.summary;
|
||||||
final name =
|
final name = summary.displayName;
|
||||||
summary.name ??
|
|
||||||
summary.canonicalAlias ??
|
|
||||||
"Unnamed Space";
|
|
||||||
final joined = joinedIds.contains(summary.roomId);
|
final joined = joinedIds.contains(summary.roomId);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
|
|
@ -255,7 +247,7 @@ class const SpaceHierarchySheet(final String spaceId, {super.key})
|
||||||
leading: AvatarOrHash(
|
leading: AvatarOrHash(
|
||||||
summary.avatarUrl,
|
summary.avatarUrl,
|
||||||
name,
|
name,
|
||||||
fallback: const Icon(Icons.workspaces),
|
fallback: Icon(summary.icon),
|
||||||
),
|
),
|
||||||
title: Text(name, maxLines: 1, overflow: .ellipsis),
|
title: Text(name, maxLines: 1, overflow: .ellipsis),
|
||||||
subtitle: summary.topic == null
|
subtitle: summary.topic == null
|
||||||
|
|
@ -390,7 +382,7 @@ class const SpaceHierarchySheet(final String spaceId, {super.key})
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
space?.name ?? space?.canonicalAlias ?? "Unnamed Space",
|
space?.displayName ?? "Unnamed Space",
|
||||||
overflow: .ellipsis,
|
overflow: .ellipsis,
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue