parent
8d944831f5
commit
b71a7f0bd6
10 changed files with 519 additions and 98 deletions
|
|
@ -28,6 +28,7 @@ import "package:nexus/models/requests/get_event_context.dart";
|
||||||
import "package:nexus/models/requests/get_mentions.dart";
|
import "package:nexus/models/requests/get_mentions.dart";
|
||||||
import "package:nexus/models/requests/get_related_events.dart";
|
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_space_hierarchy.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/requests/oauth/exchange_token.dart";
|
import "package:nexus/models/requests/oauth/exchange_token.dart";
|
||||||
|
|
@ -47,6 +48,7 @@ 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_metadata.dart";
|
import "package:nexus/models/room_metadata.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/spec_versions_response.dart";
|
import "package:nexus/models/spec_versions_response.dart";
|
||||||
import "package:nexus/src/third_party/gomuks.g.dart";
|
import "package:nexus/src/third_party/gomuks.g.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
|
@ -194,6 +196,11 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
Future<String> createRoom(CreateRoomRequest request) async =>
|
Future<String> createRoom(CreateRoomRequest request) async =>
|
||||||
(await _sendCommand("create_room", request.toJson()))["room_id"];
|
(await _sendCommand("create_room", request.toJson()))["room_id"];
|
||||||
|
|
||||||
|
Future<SpaceHierarchy> getSpaceHierarchy(
|
||||||
|
GetSpaceHierarchyRequest request,
|
||||||
|
) async =>
|
||||||
|
.fromJson(await _sendCommand("get_space_hierarchy", 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()));
|
||||||
|
|
||||||
|
|
|
||||||
34
lib/controllers/space_hierarchy.dart
Normal file
34
lib/controllers/space_hierarchy.dart
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/client.dart";
|
||||||
|
import "package:nexus/models/space_hierarchy.dart";
|
||||||
|
|
||||||
|
class SpaceHierarchyController(final String spaceId)
|
||||||
|
extends AsyncNotifier<SpaceHierarchy> {
|
||||||
|
@override
|
||||||
|
Future<SpaceHierarchy> build() => ref
|
||||||
|
.read(ClientController.provider.notifier)
|
||||||
|
.getSpaceHierarchy(.new(roomId: spaceId, maxDepth: 1));
|
||||||
|
|
||||||
|
Future<void> loadMore() async {
|
||||||
|
final current = await future;
|
||||||
|
if (current.nextBatch == null) return;
|
||||||
|
|
||||||
|
final next = await ref
|
||||||
|
.read(ClientController.provider.notifier)
|
||||||
|
.getSpaceHierarchy(
|
||||||
|
.new(roomId: spaceId, maxDepth: 1, from: current.nextBatch),
|
||||||
|
);
|
||||||
|
|
||||||
|
state = AsyncData(
|
||||||
|
current.copyWith(
|
||||||
|
rooms: current.rooms.addAll(next.rooms),
|
||||||
|
nextBatch: next.nextBatch,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static final provider = AsyncNotifierProvider.family
|
||||||
|
.autoDispose<SpaceHierarchyController, SpaceHierarchy, String>(
|
||||||
|
SpaceHierarchyController.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
39
lib/helpers/extensions/show_leave_room_sheet.dart
Normal file
39
lib/helpers/extensions/show_leave_room_sheet.dart
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:material_ui/material_ui.dart";
|
||||||
|
import "package:nexus/controllers/client.dart";
|
||||||
|
import "package:nexus/helpers/extensions/show_adaptive_sheet.dart";
|
||||||
|
import "package:nexus/helpers/extensions/with_progress_snackbar.dart";
|
||||||
|
import "package:nexus/widgets/sheet_layout.dart";
|
||||||
|
|
||||||
|
extension ShowLeaveRoomSheet on BuildContext {
|
||||||
|
/// Asks the user to confirm leaving the room, then leaves it.
|
||||||
|
Future<void> showLeaveRoomSheet(String roomId, {required String name}) =>
|
||||||
|
showAdaptiveSheet(
|
||||||
|
(context) => SheetLayout(
|
||||||
|
title: const Text("Leave Room"),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: Navigator.of(context).pop,
|
||||||
|
child: const Text("Cancel"),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
final client = ProviderScope.containerOf(
|
||||||
|
context,
|
||||||
|
).read(ClientController.provider.notifier);
|
||||||
|
context.withProgressSnackbar(
|
||||||
|
"Leaving $name...",
|
||||||
|
() => client.leaveRoom(roomId),
|
||||||
|
);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
child: const Text("Leave"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
children: [Text("Are you sure you want to leave \"$name\"?")],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
19
lib/models/requests/get_space_hierarchy.dart
Normal file
19
lib/models/requests/get_space_hierarchy.dart
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
|
|
||||||
|
part "get_space_hierarchy.freezed.dart";
|
||||||
|
part "get_space_hierarchy.g.dart";
|
||||||
|
|
||||||
|
@Freezed(toJson: false, fromJson: false)
|
||||||
|
@JsonSerializable(includeIfNull: false)
|
||||||
|
class const GetSpaceHierarchyRequest({
|
||||||
|
required final String roomId,
|
||||||
|
final String? from,
|
||||||
|
final int limit = 50,
|
||||||
|
final int? maxDepth,
|
||||||
|
final bool? suggestedOnly,
|
||||||
|
}) with _$GetSpaceHierarchyRequest {
|
||||||
|
Map<String, Object?> toJson() => _$GetSpaceHierarchyRequestToJson(this);
|
||||||
|
|
||||||
|
factory GetSpaceHierarchyRequest.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$GetSpaceHierarchyRequestFromJson(json);
|
||||||
|
}
|
||||||
45
lib/models/space_hierarchy.dart
Normal file
45
lib/models/space_hierarchy.dart
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
|
import "package:nexus/models/room_summary.dart";
|
||||||
|
|
||||||
|
part "space_hierarchy.freezed.dart";
|
||||||
|
part "space_hierarchy.g.dart";
|
||||||
|
|
||||||
|
@Freezed(toJson: false, fromJson: false)
|
||||||
|
@JsonSerializable(createToJson: false)
|
||||||
|
class const SpaceHierarchy({
|
||||||
|
final IList<SpaceHierarchyRoom> rooms = const .empty(),
|
||||||
|
final String? nextBatch,
|
||||||
|
}) with _$SpaceHierarchy {
|
||||||
|
factory SpaceHierarchy.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$SpaceHierarchyFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Freezed(toJson: false, fromJson: false)
|
||||||
|
@JsonSerializable(createToJson: false)
|
||||||
|
class const SpaceHierarchyRoom({
|
||||||
|
@JsonKey(readValue: SpaceHierarchyRoom.readEntry)
|
||||||
|
required final RoomSummary summary,
|
||||||
|
@JsonKey(name: "children_state")
|
||||||
|
final IList<SpaceHierarchyChild> children = const .empty(),
|
||||||
|
}) with _$SpaceHierarchyRoom {
|
||||||
|
static Object? readEntry(Map json, String _) => json;
|
||||||
|
|
||||||
|
factory SpaceHierarchyRoom.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$SpaceHierarchyRoomFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Freezed(toJson: false, fromJson: false)
|
||||||
|
@JsonSerializable(createToJson: false)
|
||||||
|
class const SpaceHierarchyChild({
|
||||||
|
@JsonKey(name: "state_key") required final String roomId,
|
||||||
|
@JsonKey(readValue: SpaceHierarchyChild.readContent)
|
||||||
|
final IList<String> via = const .empty(),
|
||||||
|
@JsonKey(readValue: SpaceHierarchyChild.readContent)
|
||||||
|
final bool suggested = false,
|
||||||
|
}) with _$SpaceHierarchyChild {
|
||||||
|
static Object? readContent(Map json, String key) => json["content"]?[key];
|
||||||
|
|
||||||
|
factory SpaceHierarchyChild.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$SpaceHierarchyChildFromJson(json);
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ import "dart:math";
|
||||||
|
|
||||||
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:nexus/helpers/extensions/get_all_child_rooms.dart";
|
import "package:nexus/helpers/extensions/get_all_child_rooms.dart";
|
||||||
import "package:nexus/helpers/extensions/room_destinations.dart";
|
import "package:nexus/helpers/extensions/room_destinations.dart";
|
||||||
|
|
@ -16,6 +17,7 @@ import "package:nexus/widgets/error_dialog.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/room_menu_button.dart";
|
import "package:nexus/widgets/room_menu_button.dart";
|
||||||
|
import "package:nexus/widgets/space_hierarchy_sheet.dart";
|
||||||
import "package:nexus/controllers/key.dart";
|
import "package:nexus/controllers/key.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";
|
||||||
|
|
@ -252,16 +254,36 @@ class const RoomsPane({super.key}) extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
body: MaterialUiCompatibilityBridge(
|
body: MaterialUiCompatibilityBridge(
|
||||||
child: Builder(
|
child: Builder(
|
||||||
builder: (context) => old_mat.Theme(
|
builder: (context) {
|
||||||
data: old_mat.Theme.of(context).copyWith(
|
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,
|
||||||
|
),
|
||||||
|
// Otherwise the FAB label falls back to Roboto
|
||||||
|
M3ETheme.defaults(theme.colorScheme).copyWith(
|
||||||
|
typography: .defaultFor(
|
||||||
|
theme.brightness,
|
||||||
|
baseOverride: theme.textTheme,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: NavigationRailM3E(
|
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,
|
expandedWidth: double.infinity,
|
||||||
scrollable: true,
|
scrollable: true,
|
||||||
background: Colors.transparent,
|
background: Colors.transparent,
|
||||||
|
|
@ -304,7 +326,9 @@ class const RoomsPane({super.key}) extends ConsumerWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
destinations: subSpace.children.toDestinations(isDm: isDm),
|
destinations: subSpace.children.toDestinations(
|
||||||
|
isDm: isDm,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -319,7 +343,8 @@ class const RoomsPane({super.key}) extends ConsumerWidget {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
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:nexus/helpers/extensions/get_localpart.dart";
|
import "package:nexus/helpers/extensions/get_localpart.dart";
|
||||||
import "package:nexus/helpers/extensions/with_progress_snackbar.dart";
|
import "package:nexus/helpers/extensions/show_leave_room_sheet.dart";
|
||||||
import "package:nexus/helpers/extensions/show_adaptive_sheet.dart";
|
import "package:nexus/helpers/extensions/show_adaptive_sheet.dart";
|
||||||
import "package:nexus/widgets/sheet_layout.dart";
|
import "package:nexus/widgets/sheet_layout.dart";
|
||||||
import "package:flutter/services.dart";
|
import "package:flutter/services.dart";
|
||||||
|
|
@ -123,31 +123,9 @@ final class const RoomMenuButton(
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
onTap: () => context.showAdaptiveSheet(
|
onTap: () => context.showLeaveRoomSheet(
|
||||||
(context) => SheetLayout(
|
room!.metadata!.id,
|
||||||
title: Text("Leave Room"),
|
name: room!.metadata?.name ?? "Unnamed Room",
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: Navigator.of(context).pop,
|
|
||||||
child: Text("Cancel"),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
context.withProgressSnackbar(
|
|
||||||
"Leaving room...",
|
|
||||||
() => client.leaveRoom(room!.metadata!.id),
|
|
||||||
);
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
child: Text("Leave"),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Are you sure you want to leave \"${room!.metadata?.name ?? "Unnamed Room"}\"?",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Icon(Icons.logout, color: danger),
|
leading: Icon(Icons.logout, color: danger),
|
||||||
|
|
|
||||||
263
lib/widgets/space_hierarchy_sheet.dart
Normal file
263
lib/widgets/space_hierarchy_sheet.dart
Normal file
|
|
@ -0,0 +1,263 @@
|
||||||
|
import "package:collection/collection.dart";
|
||||||
|
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: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/show_adaptive_sheet.dart";
|
||||||
|
import "package:nexus/helpers/extensions/show_leave_room_sheet.dart";
|
||||||
|
import "package:nexus/main.dart";
|
||||||
|
import "package:nexus/models/content/create.dart";
|
||||||
|
import "package:nexus/models/space_hierarchy.dart";
|
||||||
|
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||||
|
import "package:nexus/widgets/expandable_image.dart";
|
||||||
|
import "package:nexus/widgets/loading.dart";
|
||||||
|
import "package:nexus/widgets/room_summary_sheet.dart";
|
||||||
|
import "package:nexus/widgets/sheet_layout.dart";
|
||||||
|
|
||||||
|
class const SpaceHierarchySheet(final String spaceId, {super.key})
|
||||||
|
extends ConsumerWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final joinedIds = ref.watch(
|
||||||
|
RoomsController.provider.select((rooms) => rooms.keys.toISet()),
|
||||||
|
);
|
||||||
|
|
||||||
|
void showSummary(SpaceHierarchyRoom room, SpaceHierarchyChild child) =>
|
||||||
|
context.showAdaptiveSheet(
|
||||||
|
(_) => RoomSummarySheet(
|
||||||
|
room.summary,
|
||||||
|
via: joinedIds.contains(room.summary.roomId) ? null : child.via,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget membershipButton(
|
||||||
|
SpaceHierarchyRoom room,
|
||||||
|
SpaceHierarchyChild child, {
|
||||||
|
required String name,
|
||||||
|
}) => joinedIds.contains(room.summary.roomId)
|
||||||
|
? TextButton(
|
||||||
|
onPressed: () =>
|
||||||
|
context.showLeaveRoomSheet(room.summary.roomId, name: name),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
child: const Text("Leave"),
|
||||||
|
)
|
||||||
|
: TextButton(
|
||||||
|
onPressed: () => context.joinRoom(
|
||||||
|
.new(roomIdOrAlias: room.summary.roomId, via: child.via),
|
||||||
|
name: name,
|
||||||
|
),
|
||||||
|
child: const Text("Join"),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget level(
|
||||||
|
String spaceId,
|
||||||
|
SpaceHierarchy hierarchy,
|
||||||
|
ISet<String> ancestors,
|
||||||
|
) => HookConsumer(
|
||||||
|
builder: (context, ref, _) {
|
||||||
|
final loadingMore = useState(false);
|
||||||
|
|
||||||
|
final byId = IMap.fromValues(
|
||||||
|
keyMapper: (room) => room.summary.roomId,
|
||||||
|
values: hierarchy.rooms,
|
||||||
|
);
|
||||||
|
final children = (byId[spaceId]?.children ?? const .empty())
|
||||||
|
.map(
|
||||||
|
(child) => switch (byId[child.roomId]) {
|
||||||
|
// Skip spaces that contain themselves somewhere up the tree.
|
||||||
|
final room? when !ancestors.contains(child.roomId) => (
|
||||||
|
child,
|
||||||
|
room,
|
||||||
|
),
|
||||||
|
_ => null,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.nonNulls
|
||||||
|
// Suggested rooms first, then sorted by member count
|
||||||
|
.sorted((a, b) {
|
||||||
|
if (a.$1.suggested != b.$1.suggested) {
|
||||||
|
return a.$1.suggested ? -1 : 1;
|
||||||
|
}
|
||||||
|
return (b.$2.summary.joinedMembers ?? 0).compareTo(
|
||||||
|
a.$2.summary.joinedMembers ?? 0,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: .stretch,
|
||||||
|
children: [
|
||||||
|
for (final (child, room) in children)
|
||||||
|
if (room.summary.roomType != RoomType.space)
|
||||||
|
Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final summary = room.summary;
|
||||||
|
final name =
|
||||||
|
summary.name ??
|
||||||
|
summary.canonicalAlias ??
|
||||||
|
"Unnamed Room";
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
leading: AvatarOrHash(
|
||||||
|
summary.avatarUrl,
|
||||||
|
name,
|
||||||
|
fallback: const Icon(Icons.numbers),
|
||||||
|
),
|
||||||
|
title: Text(name, maxLines: 1, overflow: .ellipsis),
|
||||||
|
subtitle: summary.topic == null
|
||||||
|
? null
|
||||||
|
: Text(
|
||||||
|
summary.topic!,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: .ellipsis,
|
||||||
|
),
|
||||||
|
onTap: () => showSummary(room, child),
|
||||||
|
trailing: membershipButton(room, child, name: name),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
for (final (child, room) in children)
|
||||||
|
if (room.summary.roomType == RoomType.space)
|
||||||
|
HookConsumer(
|
||||||
|
builder: (context, ref, _) {
|
||||||
|
final expanded = useState(false);
|
||||||
|
final summary = room.summary;
|
||||||
|
final name =
|
||||||
|
summary.name ??
|
||||||
|
summary.canonicalAlias ??
|
||||||
|
"Unnamed Space";
|
||||||
|
final joined = joinedIds.contains(summary.roomId);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: .stretch,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
onTap: joined
|
||||||
|
? () => expanded.value = !expanded.value
|
||||||
|
: () => showSummary(room, child),
|
||||||
|
leading: AvatarOrHash(
|
||||||
|
summary.avatarUrl,
|
||||||
|
name,
|
||||||
|
fallback: const Icon(Icons.workspaces),
|
||||||
|
),
|
||||||
|
title: Text(name, maxLines: 1, overflow: .ellipsis),
|
||||||
|
subtitle: summary.topic == null
|
||||||
|
? null
|
||||||
|
: Text(
|
||||||
|
summary.topic!,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: .ellipsis,
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: .min,
|
||||||
|
children: [
|
||||||
|
if (joined)
|
||||||
|
AnimatedRotation(
|
||||||
|
turns: expanded.value ? 0.5 : 0,
|
||||||
|
duration: Durations.medium2,
|
||||||
|
child: const Icon(Icons.expand_more),
|
||||||
|
),
|
||||||
|
membershipButton(room, child, name: name),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AnimatedSize(
|
||||||
|
duration: Durations.medium2,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
alignment: .topCenter,
|
||||||
|
child: joined && expanded.value
|
||||||
|
? Padding(
|
||||||
|
// Indented so it's clear which rooms are in which space
|
||||||
|
padding: const .only(left: 16),
|
||||||
|
child: ref
|
||||||
|
.watch(
|
||||||
|
SpaceHierarchyController.provider(
|
||||||
|
summary.roomId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.betterWhen(
|
||||||
|
loading: () =>
|
||||||
|
const Loading(dimension: 24),
|
||||||
|
data: (hierarchy) => level(
|
||||||
|
summary.roomId,
|
||||||
|
hierarchy,
|
||||||
|
ancestors.add(child.roomId),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox(width: .infinity),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (hierarchy.nextBatch != null)
|
||||||
|
Center(
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: loadingMore.value
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
loadingMore.value = true;
|
||||||
|
await ref
|
||||||
|
.read(
|
||||||
|
SpaceHierarchyController.provider(spaceId)
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.loadMore()
|
||||||
|
.onError(showError);
|
||||||
|
if (context.mounted) loadingMore.value = false;
|
||||||
|
},
|
||||||
|
child: const Text("Load more"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return ref
|
||||||
|
.watch(SpaceHierarchyController.provider(spaceId))
|
||||||
|
.betterWhen(
|
||||||
|
data: (hierarchy) {
|
||||||
|
final space = hierarchy.rooms
|
||||||
|
.where((room) => room.summary.roomId == spaceId)
|
||||||
|
.firstOrNull
|
||||||
|
?.summary;
|
||||||
|
|
||||||
|
return SheetLayout(
|
||||||
|
title: Row(
|
||||||
|
spacing: 12,
|
||||||
|
children: [
|
||||||
|
if (space?.avatarUrl case final avatarUrl?)
|
||||||
|
ExpandableImage(
|
||||||
|
.new(mxc: avatarUrl),
|
||||||
|
child: AvatarOrHash(avatarUrl, "", dimension: 32),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
space?.name ?? space?.canonicalAlias ?? "Unnamed Space",
|
||||||
|
overflow: .ellipsis,
|
||||||
|
maxLines: 3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: Navigator.of(context).pop,
|
||||||
|
child: const Text("Close"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
level(spaceId, hierarchy, .new([spaceId])),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
pubspec.lock
24
pubspec.lock
|
|
@ -314,12 +314,13 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
fab_m3e:
|
fab_m3e:
|
||||||
dependency: transitive
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
name: fab_m3e
|
path: "packages/fab_m3e"
|
||||||
sha256: e4f5abfa3c8c092005449d56dcac45b85e2dbe9c32789d672c5ed71428e43b59
|
ref: HEAD
|
||||||
url: "https://pub.dev"
|
resolved-ref: "35ff1cea8e1a23523428710b27fc5d190ee81e96"
|
||||||
source: hosted
|
url: "https://github.com/Henry-Hiles/material_3_expressive"
|
||||||
|
source: git
|
||||||
version: "0.1.1"
|
version: "0.1.1"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
|
|
@ -863,12 +864,13 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
m3e_design:
|
m3e_design:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: m3e_design
|
path: "packages/m3e_design"
|
||||||
sha256: "15ff0ef4c43553d855c5e866a9aee8231d44919fe2bb354b1259337bdfd659b4"
|
ref: HEAD
|
||||||
url: "https://pub.dev"
|
resolved-ref: "35ff1cea8e1a23523428710b27fc5d190ee81e96"
|
||||||
source: hosted
|
url: "https://github.com/Henry-Hiles/material_3_expressive"
|
||||||
|
source: git
|
||||||
version: "0.2.1"
|
version: "0.2.1"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
|
|
@ -1011,7 +1013,7 @@ packages:
|
||||||
description:
|
description:
|
||||||
path: "packages/navigation_rail_m3e"
|
path: "packages/navigation_rail_m3e"
|
||||||
ref: HEAD
|
ref: HEAD
|
||||||
resolved-ref: da860a413d5cf78b9c9c967215250bcdbfe5191d
|
resolved-ref: "35ff1cea8e1a23523428710b27fc5d190ee81e96"
|
||||||
url: "https://github.com/Henry-Hiles/material_3_expressive"
|
url: "https://github.com/Henry-Hiles/material_3_expressive"
|
||||||
source: git
|
source: git
|
||||||
version: "0.3.5"
|
version: "0.3.5"
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,14 @@ environment:
|
||||||
sdk: 3.13.3
|
sdk: 3.13.3
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
|
m3e_design:
|
||||||
|
git:
|
||||||
|
url: https://github.com/Henry-Hiles/material_3_expressive
|
||||||
|
path: packages/m3e_design
|
||||||
|
fab_m3e:
|
||||||
|
git:
|
||||||
|
url: https://github.com/Henry-Hiles/material_3_expressive
|
||||||
|
path: packages/fab_m3e
|
||||||
dynamic_color: 2.1.0
|
dynamic_color: 2.1.0
|
||||||
unifiedpush_linux:
|
unifiedpush_linux:
|
||||||
git:
|
git:
|
||||||
|
|
@ -79,6 +87,7 @@ dependencies:
|
||||||
media_kit_libs_video: 1.0.7
|
media_kit_libs_video: 1.0.7
|
||||||
measure_size: 5.0.2
|
measure_size: 5.0.2
|
||||||
m3e_buttons: 1.0.3
|
m3e_buttons: 1.0.3
|
||||||
|
m3e_design: 0.2.1
|
||||||
navigation_rail_m3e:
|
navigation_rail_m3e:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Henry-Hiles/material_3_expressive
|
url: https://github.com/Henry-Hiles/material_3_expressive
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue