working sidebar

This commit is contained in:
Henry Hiles 2026-01-27 14:14:04 +00:00
commit 85d96b80bc
No known key found for this signature in database
13 changed files with 491 additions and 436 deletions

View file

@ -1,4 +1,5 @@
import "dart:io";
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter/material.dart";
import "package:window_manager/window_manager.dart";
@ -7,7 +8,7 @@ class Appbar extends StatelessWidget implements PreferredSizeWidget {
final Widget? title;
final Color? backgroundColor;
final double? scrolledUnderElevation;
final List<Widget> actions;
final IList<Widget> actions;
const Appbar({
super.key,
@ -15,7 +16,7 @@ class Appbar extends StatelessWidget implements PreferredSizeWidget {
this.backgroundColor,
this.scrolledUnderElevation,
this.leading,
this.actions = const [],
this.actions = const IList.empty(),
});
@override

View file

@ -1,122 +1,124 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:matrix/matrix.dart";
import "package:nexus/helpers/extensions/room_to_children.dart";
import "package:nexus/models/room.dart";
import "package:nexus/widgets/form_text_input.dart";
class RoomMenu extends StatelessWidget {
final Room room;
const RoomMenu(this.room, {super.key});
final IList<Room> children;
const RoomMenu(this.room, {this.children = const IList.empty(), super.key});
@override
Widget build(BuildContext context) {
final danger = Theme.of(context).colorScheme.error;
void markRead(String roomId) async {
for (final child in await room.getAllChildren()) {
await child.roomData.setReadMarker(
child.roomData.lastEvent?.eventId,
mRead: child.roomData.lastEvent?.eventId,
);
// TODO: Set parent read
for (final child in children) {
// await child.setReadMarker( TODO: Set children read
// child.roomData.lastEvent?.eventId,
// mRead: child.roomData.lastEvent?.eventId,
// );
}
}
return PopupMenuButton(
itemBuilder: (_) => [
PopupMenuItem(
onTap: () async {
final link = await room.matrixToInviteLink();
await Clipboard.setData(ClipboardData(text: link.toString()));
},
child: ListTile(leading: Icon(Icons.link), title: Text("Copy Link")),
),
PopupMenuItem(
onTap: () => markRead(room.id),
child: ListTile(
leading: Icon(Icons.check),
title: Text("Mark as Read"),
),
),
PopupMenuItem(
onTap: () => showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text("Leave Room"),
content: Text(
"Are you sure you want to leave \"${room.getLocalizedDisplayname()}\"?",
),
actions: [
TextButton(
onPressed: Navigator.of(context).pop,
child: Text("Cancel"),
),
TextButton(
onPressed: () async {
Navigator.of(context).pop();
final snackbar = ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text("Leaving room...")));
await room.leave();
snackbar.close();
},
child: Text("Leave"),
),
],
),
),
child: ListTile(
leading: Icon(Icons.logout, color: danger),
title: Text("Leave", style: TextStyle(color: danger)),
),
),
PopupMenuItem(
onTap: () => showDialog(
context: context,
builder: (context) => HookBuilder(
builder: (_) {
final reasonController = useTextEditingController();
return AlertDialog(
title: Text("Report"),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Report this room to your server administrators, who can take action like banning this room.",
),
// PopupMenuItem(
// onTap: () async {
// final link = await room.matrixToInviteLink();
// await Clipboard.setData(ClipboardData(text: link.toString()));
// },
// child: ListTile(leading: Icon(Icons.link), title: Text("Copy Link")),
// ),
// PopupMenuItem(
// onTap: () => markRead(room.id),
// child: ListTile(
// leading: Icon(Icons.check),
// title: Text("Mark as Read"),
// ),
// ),
// PopupMenuItem(
// onTap: () => showDialog(
// context: context,
// builder: (context) => AlertDialog(
// title: Text("Leave Room"),
// content: Text(
// "Are you sure you want to leave \"${room.getLocalizedDisplayname()}\"?",
// ),
// actions: [
// TextButton(
// onPressed: Navigator.of(context).pop,
// child: Text("Cancel"),
// ),
// TextButton(
// onPressed: () async {
// Navigator.of(context).pop();
// final snackbar = ScaffoldMessenger.of(
// context,
// ).showSnackBar(SnackBar(content: Text("Leaving room...")));
// await room.leave();
// snackbar.close();
// },
// child: Text("Leave"),
// ),
// ],
// ),
// ),
// child: ListTile(
// leading: Icon(Icons.logout, color: danger),
// title: Text("Leave", style: TextStyle(color: danger)),
// ),
// ),
// PopupMenuItem(
// onTap: () => showDialog(
// context: context,
// builder: (context) => HookBuilder(
// builder: (_) {
// final reasonController = useTextEditingController();
// return AlertDialog(
// title: Text("Report"),
// content: Column(
// mainAxisSize: MainAxisSize.min,
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Text(
// "Report this room to your server administrators, who can take action like banning this room.",
// ),
SizedBox(height: 12),
FormTextInput(
required: false,
capitalize: true,
controller: reasonController,
title: "Reason for report (optional)",
),
],
),
actions: [
TextButton(
onPressed: Navigator.of(context).pop,
child: Text("Cancel"),
),
TextButton(
onPressed: () {
room.client.reportRoom(room.id, reasonController.text);
Navigator.of(context).pop();
},
child: Text("Report"),
),
],
);
},
),
),
child: ListTile(
leading: Icon(Icons.report, color: danger),
title: Text("Report", style: TextStyle(color: danger)),
),
),
// SizedBox(height: 12),
// FormTextInput(
// required: false,
// capitalize: true,
// controller: reasonController,
// title: "Reason for report (optional)",
// ),
// ],
// ),
// actions: [
// TextButton(
// onPressed: Navigator.of(context).pop,
// child: Text("Cancel"),
// ),
// TextButton(
// onPressed: () {
// room.client.reportRoom(room.id, reasonController.text);
// Navigator.of(context).pop();
// },
// child: Text("Report"),
// ),
// ],
// );
// },
// ),
// ),
// child: ListTile(
// leading: Icon(Icons.report, color: danger),
// title: Text("Report", style: TextStyle(color: danger)),
// ),
// ),
],
);
}

View file

@ -1,4 +1,3 @@
import "package:collection/collection.dart";
import "package:flutter/material.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
@ -6,8 +5,6 @@ import "package:nexus/controllers/client_controller.dart";
import "package:nexus/controllers/key_controller.dart";
import "package:nexus/controllers/selected_space_controller.dart";
import "package:nexus/controllers/spaces_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/helpers/extensions/get_headers.dart";
import "package:nexus/helpers/extensions/join_room_with_snackbars.dart";
import "package:nexus/pages/settings_page.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
@ -22,228 +19,208 @@ class Sidebar extends HookConsumerWidget {
final selectedSpaceProvider = KeyController.provider(
KeyController.spaceKey,
);
final selectedSpace = ref.watch(selectedSpaceProvider);
final selectedSpaceNotifier = ref.watch(selectedSpaceProvider.notifier);
final selectedSpaceId = ref.watch(selectedSpaceProvider);
final selectedSpaceIdNotifier = ref.watch(selectedSpaceProvider.notifier);
final selectedRoomController = KeyController.provider(
KeyController.roomKey,
);
final selectedRoom = ref.watch(selectedRoomController);
final selectedRoomNotifier = ref.watch(selectedRoomController.notifier);
final selectedRoomId = ref.watch(selectedRoomController);
final selectedRoomIdNotifier = ref.watch(selectedRoomController.notifier);
final spaces = ref.watch(SpacesController.provider);
final indexOfSelected = spaces.indexWhere(
(space) => space.id == selectedSpaceId,
);
final selectedIndex = indexOfSelected == -1 ? 0 : indexOfSelected;
final selectedSpace = ref.watch(SelectedSpaceController.provider);
final indexOfSelectedRoom = selectedSpace.children.indexWhere(
(room) => room.metadata?.id == selectedRoomId,
);
final selectedRoomIndex = indexOfSelected == -1
? selectedSpace.children.isEmpty
? null
: 0
: indexOfSelectedRoom;
return Drawer(
shape: Border(),
child: Row(
children: [
ref
.watch(SpacesController.provider)
.when(
loading: SizedBox.shrink,
error: (error, stack) {
debugPrintStack(label: error.toString(), stackTrace: stack);
throw error;
},
data: (spaces) {
final indexOfSelected = spaces.indexWhere(
(space) => space.id == selectedSpace,
);
final selectedIndex = indexOfSelected == -1
? 0
: indexOfSelected;
return NavigationRail(
scrollable: true,
onDestinationSelected: (value) {
selectedSpaceNotifier.set(spaces[value].id);
selectedRoomNotifier.set(
spaces[value].children.firstOrNull?.roomData.id,
);
},
destinations: spaces
.map(
(space) => NavigationRailDestination(
icon: AvatarOrHash(
space.avatar,
fallback: space.icon == null
? null
: Icon(space.icon),
space.title,
headers: space.client.headers,
hasBadge:
space.children.firstWhereOrNull(
(room) => room.roomData.hasNewMessages,
) !=
null,
),
label: Text(space.title),
padding: EdgeInsets.only(top: 4),
),
)
.toList(),
selectedIndex: selectedIndex,
trailingAtBottom: true,
trailing: Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Column(
spacing: 8,
children: [
PopupMenuButton(
itemBuilder: (_) => [
PopupMenuItem(
onTap: () => showDialog(
context: context,
builder: (alertContext) => HookBuilder(
builder: (_) {
final roomAlias =
useTextEditingController();
return AlertDialog(
title: Text("Join a Room"),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
"Enter the room alias, ID, or a Matrix.to link.",
),
SizedBox(height: 12),
FormTextInput(
required: false,
capitalize: true,
controller: roomAlias,
title: "#room:server",
),
],
),
actions: [
TextButton(
onPressed: Navigator.of(
context,
).pop,
child: Text("Cancel"),
),
TextButton(
onPressed: () async {
Navigator.of(alertContext).pop();
final client = await ref.watch(
ClientController
.provider
.future,
);
if (context.mounted) {
client.joinRoomWithSnackBars(
context,
roomAlias.text,
ref,
);
}
},
child: Text("Join"),
),
],
);
},
),
),
child: ListTile(
title: Text(
"Join an existing room (or space)",
),
leading: Icon(Icons.numbers),
),
),
PopupMenuItem(
onTap: () {},
child: ListTile(
title: Text("Create a new room"),
leading: Icon(Icons.add),
),
),
],
icon: Icon(Icons.add),
),
IconButton(
onPressed: () => showDialog(
context: context,
builder: (context) =>
AlertDialog(title: Text("To-do")),
),
icon: Icon(Icons.explore),
),
IconButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => SettingsPage()),
),
icon: Icon(Icons.settings),
),
],
),
NavigationRail(
scrollable: true,
onDestinationSelected: (value) {
selectedSpaceIdNotifier.set(spaces[value].id);
selectedRoomIdNotifier.set(
spaces[value].children.firstOrNull?.metadata?.id,
);
},
destinations: spaces
.map(
(space) => NavigationRailDestination(
icon: AvatarOrHash(
null, // TODO: Url
fallback: space.icon == null ? null : Icon(space.icon),
space.title,
headers: {}, // TODO
hasBadge: false,
// space.children.firstWhereOrNull( TODO
// (room) => room.roomData.hasNewMessages,
// ) !=
// null,
),
);
},
),
Expanded(
child: ref
.watch(SelectedSpaceController.provider)
.betterWhen(
data: (space) {
final indexOfSelected = space.children.indexWhere(
(room) => room.roomData.id == selectedRoom,
);
final selectedIndex = indexOfSelected == -1
? space.children.isEmpty
? null
: 0
: indexOfSelected;
return Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
leading: AvatarOrHash(
space.avatar,
fallback: space.icon == null
? null
: Icon(space.icon),
space.title,
headers: space.client.headers,
),
title: Text(
space.title,
overflow: TextOverflow.ellipsis,
),
backgroundColor: Colors.transparent,
actions: [
if (space.roomData != null) RoomMenu(space.roomData!),
],
),
body: NavigationRail(
scrollable: true,
backgroundColor: Colors.transparent,
extended: true,
selectedIndex: selectedIndex,
destinations: space.children
.map(
(room) => NavigationRailDestination(
label: Text(room.title),
icon: AvatarOrHash(
hasBadge: room.roomData.hasNewMessages,
room.avatar,
room.title,
fallback: selectedSpace == "dms"
? null
: Icon(Icons.numbers),
headers: space.client.headers,
label: Text(space.title),
padding: EdgeInsets.only(top: 4),
),
)
.toList(),
selectedIndex: selectedIndex,
trailingAtBottom: true,
trailing: Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Column(
spacing: 8,
children: [
PopupMenuButton(
itemBuilder: (_) => [
PopupMenuItem(
onTap: () => showDialog(
context: context,
builder: (alertContext) => HookBuilder(
builder: (_) {
final roomAlias = useTextEditingController();
return AlertDialog(
title: Text("Join a Room"),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Enter the room alias, ID, or a Matrix.to link.",
),
SizedBox(height: 12),
FormTextInput(
required: false,
capitalize: true,
controller: roomAlias,
title: "#room:server",
),
],
),
),
)
.toList(),
onDestinationSelected: (value) => selectedRoomNotifier
.set(space.children[value].roomData.id),
actions: [
TextButton(
onPressed: Navigator.of(context).pop,
child: Text("Cancel"),
),
TextButton(
onPressed: () async {
Navigator.of(alertContext).pop();
final client = ref.watch(
ClientController.provider.notifier,
);
if (context.mounted) {
client.joinRoomWithSnackBars(
context,
roomAlias.text,
ref,
);
}
},
child: Text("Join"),
),
],
);
},
),
),
child: ListTile(
title: Text("Join an existing room (or space)"),
leading: Icon(Icons.numbers),
),
),
);
},
PopupMenuItem(
onTap: () {},
child: ListTile(
title: Text("Create a new room"),
leading: Icon(Icons.add),
),
),
],
icon: Icon(Icons.add),
),
IconButton(
onPressed: () => showDialog(
context: context,
builder: (context) => AlertDialog(title: Text("To-do")),
),
icon: Icon(Icons.explore),
),
IconButton(
onPressed: () => Navigator.of(
context,
).push(MaterialPageRoute(builder: (_) => SettingsPage())),
icon: Icon(Icons.settings),
),
],
),
),
),
Expanded(
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
leading: AvatarOrHash(
null,
// space.avatar, TODO
fallback: selectedSpace.icon == null
? null
: Icon(selectedSpace.icon),
selectedSpace.title, // TODO RM
headers: {},
// space.client.headers, TODO
),
title: Text(
selectedSpace.room?.metadata?.avatar.toString() ??
selectedSpace.title,
overflow: TextOverflow.ellipsis,
),
backgroundColor: Colors.transparent,
actions: [
if (selectedSpace.room != null) RoomMenu(selectedSpace.room!),
],
),
body: NavigationRail(
scrollable: true,
backgroundColor: Colors.transparent,
extended: true,
selectedIndex: selectedRoomIndex,
destinations: selectedSpace.children
.map(
(room) => NavigationRailDestination(
label: Text(room.metadata?.name ?? "Unnamed Room"),
icon: AvatarOrHash(
// hasBadge: room.roomData.hasNewMessages, TODO
null,
// room.avatar, TODO
room.metadata?.name ?? "Unnamed Room",
fallback: selectedSpaceId == "dms"
? null
: Icon(Icons.numbers),
headers: {},
// space.client.headers,
),
),
)
.toList(),
onDestinationSelected: (value) => selectedRoomIdNotifier.set(
selectedSpace.children[value].metadata?.id,
),
),
),
),
],
),