add room directory support
This commit is contained in:
parent
033c7f9c05
commit
e382499a2a
9 changed files with 868 additions and 245 deletions
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:nexus/controllers/invites.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/models/content/create.dart";
|
||||
import "package:nexus/models/nav_page.dart";
|
||||
import "package:nexus/models/room_summary.dart";
|
||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||
|
|
@ -120,13 +120,7 @@ class const InviteCard(final RoomSummary invite, {super.key})
|
|||
invite.avatarUrl,
|
||||
name,
|
||||
dimension: 40,
|
||||
fallback: invite.isDirect
|
||||
? null
|
||||
: Icon(
|
||||
invite.roomType == RoomType.space
|
||||
? Icons.workspaces
|
||||
: Icons.numbers,
|
||||
),
|
||||
fallback: invite.isDirect ? null : Icon(invite.icon),
|
||||
),
|
||||
title: Text(name, maxLines: 1, overflow: .ellipsis),
|
||||
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:hooks_riverpod/hooks_riverpod.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:material_adaptive_sheets/material_adaptive_sheets.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/divider_widget.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/join_sheet.dart";
|
||||
import "package:nexus/widgets/m3e_rail_theme.dart";
|
||||
import "package:nexus/widgets/room_summary_sheet.dart";
|
||||
import "package:nexus/widgets/room_menu_button.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/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 {
|
||||
@override
|
||||
String get title => "Rooms";
|
||||
|
|
@ -92,123 +90,112 @@ class const SpacesRail({super.key}) extends ConsumerWidget {
|
|||
.select((async) => async.requireValue),
|
||||
);
|
||||
|
||||
return MaterialUiCompatibilityBridge(
|
||||
child: Builder(
|
||||
builder: (context) => old_mat.Theme(
|
||||
data: old_mat.Theme.of(context).copyWith(
|
||||
extensions: [
|
||||
NavigationRailM3ETheme(
|
||||
itemCollapsedHeight: 48,
|
||||
itemVerticalGap: 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
color: NavigationRailTokensAdapter(context).containerColor,
|
||||
padding: .only(top: 16),
|
||||
child: NavigationRailM3E(
|
||||
type: .alwaysCollapse,
|
||||
labelBehavior: .alwaysHide,
|
||||
scrollable: true,
|
||||
onDestinationSelected: (value) {
|
||||
ref
|
||||
.read(
|
||||
KeyController.provider(KeyController.spaceKey).notifier,
|
||||
)
|
||||
.set(spaces[value].id);
|
||||
ref
|
||||
.read(
|
||||
KeyController.provider(KeyController.roomKey).notifier,
|
||||
)
|
||||
.set(spaces[value].children.firstOrNull?.metadata?.id);
|
||||
},
|
||||
sections: [
|
||||
.new(
|
||||
destinations: spaces.map((space) {
|
||||
final notifications = space.allChildRooms.fold(
|
||||
0,
|
||||
(sum, room) =>
|
||||
sum + (room.metadata?.unreadNotifications ?? 0),
|
||||
);
|
||||
return M3ERailTheme(
|
||||
extensions: [
|
||||
NavigationRailM3ETheme(itemCollapsedHeight: 48, itemVerticalGap: 0),
|
||||
],
|
||||
builder: (context) => Container(
|
||||
color: NavigationRailTokensAdapter(context).containerColor,
|
||||
padding: .only(top: 16),
|
||||
child: NavigationRailM3E(
|
||||
type: .alwaysCollapse,
|
||||
labelBehavior: .alwaysHide,
|
||||
scrollable: true,
|
||||
onDestinationSelected: (value) {
|
||||
ref
|
||||
.read(KeyController.provider(KeyController.spaceKey).notifier)
|
||||
.set(spaces[value].id);
|
||||
ref
|
||||
.read(KeyController.provider(KeyController.roomKey).notifier)
|
||||
.set(spaces[value].children.firstOrNull?.metadata?.id);
|
||||
},
|
||||
sections: [
|
||||
.new(
|
||||
destinations: spaces.map((space) {
|
||||
final notifications = space.allChildRooms.fold(
|
||||
0,
|
||||
(sum, room) =>
|
||||
sum + (room.metadata?.unreadNotifications ?? 0),
|
||||
);
|
||||
|
||||
return NavigationRailM3EDestination(
|
||||
badgeCount: notifications > 0
|
||||
? notifications
|
||||
: space.allChildRooms.any(
|
||||
(room) => room.metadata?.unreadMessages != 0,
|
||||
)
|
||||
? 0
|
||||
: null,
|
||||
short: true,
|
||||
icon: AvatarOrHash(
|
||||
dimension: 28,
|
||||
space.room?.metadata?.avatar,
|
||||
fallback: space.icon == null ? null : Icon(space.icon),
|
||||
space.title,
|
||||
return NavigationRailM3EDestination(
|
||||
badgeCount: notifications > 0
|
||||
? notifications
|
||||
: space.allChildRooms.any(
|
||||
(room) => room.metadata?.unreadMessages != 0,
|
||||
)
|
||||
? 0
|
||||
: null,
|
||||
short: true,
|
||||
icon: AvatarOrHash(
|
||||
dimension: 28,
|
||||
space.room?.metadata?.avatar,
|
||||
fallback: space.icon == null ? null : Icon(space.icon),
|
||||
space.title,
|
||||
),
|
||||
label: space.title,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
selectedIndex: max(
|
||||
0,
|
||||
spaces.indexWhere((space) => space.id == selectedSpaceId),
|
||||
),
|
||||
trailingAtBottom: true,
|
||||
trailing: Padding(
|
||||
padding: .only(top: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
AdaptiveMenuButton(
|
||||
tooltip: "Add a room",
|
||||
itemBuilder: (_) => [
|
||||
PopupMenuItem(
|
||||
onTap: () =>
|
||||
context.showAdaptiveSheet((_) => JoinSheet(ref)),
|
||||
child: const ListTile(
|
||||
title: Text("Join an existing room (or space)"),
|
||||
leading: Icon(Icons.numbers),
|
||||
),
|
||||
label: space.title,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
selectedIndex: max(
|
||||
0,
|
||||
spaces.indexWhere((space) => space.id == selectedSpaceId),
|
||||
),
|
||||
trailingAtBottom: true,
|
||||
trailing: Padding(
|
||||
padding: .only(top: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
AdaptiveMenuButton(
|
||||
tooltip: "Add a room",
|
||||
itemBuilder: (_) => [
|
||||
PopupMenuItem(
|
||||
onTap: () =>
|
||||
context.showAdaptiveSheet((_) => JoinSheet(ref)),
|
||||
child: const ListTile(
|
||||
title: Text("Join an existing room (or space)"),
|
||||
leading: Icon(Icons.numbers),
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
onTap: () => context.showAdaptiveSheet(
|
||||
(_) => const CreateRoomSheet(),
|
||||
),
|
||||
child: const ListTile(
|
||||
title: Text("Create a new room (or space)"),
|
||||
leading: Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
onTap: () => context.showAdaptiveSheet(
|
||||
(_) => const StartDmSheet(),
|
||||
),
|
||||
child: const ListTile(
|
||||
title: Text("Start a direct message"),
|
||||
leading: Icon(Icons.person_add),
|
||||
),
|
||||
),
|
||||
],
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
const AccountSwitcher(),
|
||||
const IconButton(
|
||||
tooltip: "Explore other rooms (Not yet implemented)",
|
||||
onPressed: null,
|
||||
icon: Icon(Icons.explore),
|
||||
PopupMenuItem(
|
||||
onTap: () => context.showAdaptiveSheet(
|
||||
(_) => const CreateRoomSheet(),
|
||||
),
|
||||
child: const ListTile(
|
||||
title: Text("Create a new room (or space)"),
|
||||
leading: Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: "Open settings",
|
||||
onPressed: () => showDialog(
|
||||
context: context,
|
||||
builder: (_) => SettingsPage(),
|
||||
PopupMenuItem(
|
||||
onTap: () => context.showAdaptiveSheet(
|
||||
(_) => const StartDmSheet(),
|
||||
),
|
||||
child: const ListTile(
|
||||
title: Text("Start a direct message"),
|
||||
leading: Icon(Icons.person_add),
|
||||
),
|
||||
icon: const Icon(Icons.settings),
|
||||
),
|
||||
],
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
const AccountSwitcher(),
|
||||
IconButton(
|
||||
tooltip: "Explore other rooms",
|
||||
onPressed: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const ExplorePage()),
|
||||
),
|
||||
icon: const Icon(Icons.explore),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: "Open settings",
|
||||
onPressed: () => showDialog(
|
||||
context: context,
|
||||
builder: (_) => SettingsPage(),
|
||||
),
|
||||
icon: const Icon(Icons.settings),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -254,111 +241,86 @@ class const RoomsPane({super.key}) extends ConsumerWidget {
|
|||
),
|
||||
]),
|
||||
),
|
||||
body: MaterialUiCompatibilityBridge(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final theme = old_mat.Theme.of(context);
|
||||
return old_mat.Theme(
|
||||
data: theme.copyWith(
|
||||
extensions: [
|
||||
NavigationRailM3ETheme(
|
||||
itemExpandedHeight: 48,
|
||||
iconLabelGap: 16,
|
||||
sectionHeaderSpacingTop: 0,
|
||||
),
|
||||
// Otherwise the FAB label falls back to Roboto
|
||||
M3ETheme.defaults(theme.colorScheme).copyWith(
|
||||
typography: .defaultFor(
|
||||
theme.brightness,
|
||||
baseOverride: theme.textTheme,
|
||||
body: M3ERailTheme(
|
||||
extensions: [
|
||||
NavigationRailM3ETheme(
|
||||
itemExpandedHeight: 48,
|
||||
iconLabelGap: 16,
|
||||
sectionHeaderSpacingTop: 0,
|
||||
),
|
||||
],
|
||||
builder: (context) => Padding(
|
||||
padding: const .only(top: 4),
|
||||
child: NavigationRailM3E(
|
||||
fab: selectedSpace.room == null
|
||||
? null
|
||||
: .new(
|
||||
icon: const Icon(Icons.explore),
|
||||
label: "Browse space",
|
||||
tooltip: "Browse and join rooms in this space",
|
||||
onPressed: () => context.showAdaptiveSheet(
|
||||
(_) => SpaceHierarchySheet(selectedSpace.id),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const .only(top: 4),
|
||||
child: NavigationRailM3E(
|
||||
fab: selectedSpace.room == null
|
||||
? null
|
||||
: .new(
|
||||
icon: const Icon(Icons.explore),
|
||||
label: "Browse space",
|
||||
tooltip: "Browse and join rooms in this space",
|
||||
onPressed: () => context.showAdaptiveSheet(
|
||||
(_) => SpaceHierarchySheet(selectedSpace.id),
|
||||
),
|
||||
),
|
||||
expandedWidth: double.infinity,
|
||||
scrollable: true,
|
||||
background: Colors.transparent,
|
||||
type: .alwaysExpand,
|
||||
selectedIndex: max(0, destinationIds.indexOf(selectedRoomId)),
|
||||
sections: [
|
||||
if (selectedSpace.room case final room?
|
||||
when spaceMetadata != null)
|
||||
.new(
|
||||
destinations: [
|
||||
room.toDestination(
|
||||
isDm: isDm,
|
||||
label: "Space timeline",
|
||||
icon: const Icon(Icons.forum),
|
||||
),
|
||||
],
|
||||
),
|
||||
.new(
|
||||
header: selectedSpace.room == null
|
||||
? null
|
||||
: const DividerWidget(Text("Rooms")),
|
||||
destinations: selectedSpace.children.toDestinations(
|
||||
isDm: isDm,
|
||||
),
|
||||
),
|
||||
...selectedSpace.subSpaces.map(
|
||||
(subSpace) => NavigationRailM3ESection(
|
||||
header: DividerWidget(
|
||||
Row(
|
||||
mainAxisSize: .min,
|
||||
spacing: 8,
|
||||
children: [
|
||||
if (subSpace.room.metadata?.avatar != null)
|
||||
AvatarOrHash(
|
||||
subSpace.room.metadata?.avatar,
|
||||
subSpace.room.metadata?.name ??
|
||||
"Unnamed Room",
|
||||
dimension: 16,
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
subSpace.room.metadata?.name ??
|
||||
"Unnamed Space",
|
||||
maxLines: 1,
|
||||
overflow: .ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
destinations: subSpace.children.toDestinations(
|
||||
isDm: isDm,
|
||||
),
|
||||
),
|
||||
expandedWidth: double.infinity,
|
||||
scrollable: true,
|
||||
background: Colors.transparent,
|
||||
type: .alwaysExpand,
|
||||
selectedIndex: max(0, destinationIds.indexOf(selectedRoomId)),
|
||||
sections: [
|
||||
if (selectedSpace.room case final room?
|
||||
when spaceMetadata != null)
|
||||
.new(
|
||||
destinations: [
|
||||
room.toDestination(
|
||||
isDm: isDm,
|
||||
label: "Space timeline",
|
||||
icon: const Icon(Icons.forum),
|
||||
),
|
||||
],
|
||||
onDestinationSelected: (value) {
|
||||
ref
|
||||
.read(
|
||||
KeyController.provider(KeyController.roomKey)
|
||||
.notifier,
|
||||
)
|
||||
.set(destinationIds[value]);
|
||||
if (Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
.new(
|
||||
header: selectedSpace.room == null
|
||||
? null
|
||||
: const DividerWidget(Text("Rooms")),
|
||||
destinations: selectedSpace.children.toDestinations(isDm: isDm),
|
||||
),
|
||||
...selectedSpace.subSpaces.map(
|
||||
(subSpace) => NavigationRailM3ESection(
|
||||
header: DividerWidget(
|
||||
Row(
|
||||
mainAxisSize: .min,
|
||||
spacing: 8,
|
||||
children: [
|
||||
if (subSpace.room.metadata?.avatar != null)
|
||||
AvatarOrHash(
|
||||
subSpace.room.metadata?.avatar,
|
||||
subSpace.room.metadata?.name ?? "Unnamed Room",
|
||||
dimension: 16,
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
subSpace.room.metadata?.name ?? "Unnamed Space",
|
||||
maxLines: 1,
|
||||
overflow: .ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
destinations: subSpace.children.toDestinations(isDm: isDm),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
],
|
||||
onDestinationSelected: (value) {
|
||||
ref
|
||||
.read(KeyController.provider(KeyController.roomKey).notifier)
|
||||
.set(destinationIds[value]);
|
||||
if (Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import "package:nexus/controllers/invites.dart";
|
|||
import "package:nexus/controllers/rooms.dart";
|
||||
import "package:nexus/helpers/extensions/get_localpart.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/widgets/avatar_or_hash.dart";
|
||||
import "package:nexus/widgets/expandable_image.dart";
|
||||
|
|
@ -44,15 +45,11 @@ class const RoomSummarySheet(
|
|||
summary.avatarUrl,
|
||||
"",
|
||||
dimension: 64,
|
||||
fallback: Icon(Icons.numbers),
|
||||
fallback: Icon(summary.icon),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
summary.name ?? summary.canonicalAlias ?? "Unnamed Room",
|
||||
overflow: .ellipsis,
|
||||
maxLines: 3,
|
||||
),
|
||||
child: Text(summary.displayName, 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/divider_text.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";
|
||||
|
||||
/// A settings screen split into categories: a list of them that opens each
|
||||
|
|
@ -170,8 +171,8 @@ class const SettingsLayout({
|
|||
)
|
||||
: Row(
|
||||
children: [
|
||||
MaterialUiCompatibilityBridge(
|
||||
child: NavigationRailM3E(
|
||||
M3ERailTheme(
|
||||
builder: (_) => NavigationRailM3E(
|
||||
type: .alwaysExpand,
|
||||
trailing: searchBar,
|
||||
scrollable: true,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import "package:nexus/controllers/rooms.dart";
|
|||
import "package:nexus/controllers/space_hierarchy.dart";
|
||||
import "package:nexus/helpers/extensions/better_when.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/with_progress_snackbar.dart";
|
||||
import "package:nexus/main.dart";
|
||||
|
|
@ -163,10 +164,7 @@ class const SpaceHierarchySheet(final String spaceId, {super.key})
|
|||
keyMapper: (room) => room.summary.roomId,
|
||||
values: hierarchy.rooms,
|
||||
);
|
||||
final spaceName =
|
||||
byId[spaceId]?.summary.name ??
|
||||
byId[spaceId]?.summary.canonicalAlias ??
|
||||
"Unnamed Space";
|
||||
final spaceName = byId[spaceId]?.summary.displayName ?? "Unnamed Space";
|
||||
final children = (byId[spaceId]?.children ?? const .empty())
|
||||
.map(
|
||||
(child) => switch (byId[child.roomId]) {
|
||||
|
|
@ -197,16 +195,13 @@ class const SpaceHierarchySheet(final String spaceId, {super.key})
|
|||
Builder(
|
||||
builder: (context) {
|
||||
final summary = room.summary;
|
||||
final name =
|
||||
summary.name ??
|
||||
summary.canonicalAlias ??
|
||||
"Unnamed Room";
|
||||
final name = summary.displayName;
|
||||
|
||||
return ListTile(
|
||||
leading: AvatarOrHash(
|
||||
summary.avatarUrl,
|
||||
name,
|
||||
fallback: const Icon(Icons.numbers),
|
||||
fallback: Icon(summary.icon),
|
||||
),
|
||||
title: Text(name, maxLines: 1, overflow: .ellipsis),
|
||||
subtitle: summary.topic == null
|
||||
|
|
@ -239,10 +234,7 @@ class const SpaceHierarchySheet(final String spaceId, {super.key})
|
|||
builder: (context, ref, _) {
|
||||
final expanded = useState(false);
|
||||
final summary = room.summary;
|
||||
final name =
|
||||
summary.name ??
|
||||
summary.canonicalAlias ??
|
||||
"Unnamed Space";
|
||||
final name = summary.displayName;
|
||||
final joined = joinedIds.contains(summary.roomId);
|
||||
|
||||
return Column(
|
||||
|
|
@ -255,7 +247,7 @@ class const SpaceHierarchySheet(final String spaceId, {super.key})
|
|||
leading: AvatarOrHash(
|
||||
summary.avatarUrl,
|
||||
name,
|
||||
fallback: const Icon(Icons.workspaces),
|
||||
fallback: Icon(summary.icon),
|
||||
),
|
||||
title: Text(name, maxLines: 1, overflow: .ellipsis),
|
||||
subtitle: summary.topic == null
|
||||
|
|
@ -390,7 +382,7 @@ class const SpaceHierarchySheet(final String spaceId, {super.key})
|
|||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
space?.name ?? space?.canonicalAlias ?? "Unnamed Space",
|
||||
space?.displayName ?? "Unnamed Space",
|
||||
overflow: .ellipsis,
|
||||
maxLines: 3,
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in a new issue