parent
3a33bf7bfe
commit
8fda43ccf8
6 changed files with 159 additions and 87 deletions
|
|
@ -61,6 +61,8 @@ When possible, use immutable collections instead of the mutable equivalent. For
|
|||
|
||||
When possible, we prefer not to create global variables or methods. You can usually replace a global variable with a Riverpod controller, and a global method with an extension method.
|
||||
|
||||
I'm also not a big fan of private methods or variables, but this is contextual.
|
||||
|
||||
## LLM/AI Assisted Contributions
|
||||
|
||||
LLM generated code is NOT allowed. All contributions should be written by humans. Using LLMs for interacting with others, e.g. for Comments, PRs, etc, is also not allowed.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import "package:nexus/controllers/portal.dart";
|
|||
import "package:nexus/main.dart";
|
||||
import "package:flutter_local_notifications/flutter_local_notifications.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:nexus/widgets/notifications_list.dart";
|
||||
import "package:nexus/controllers/notification_request.dart";
|
||||
import "package:xdg_desktop_portal/xdg_desktop_portal.dart";
|
||||
|
||||
class NotificationController
|
||||
|
|
@ -34,16 +34,10 @@ class NotificationController
|
|||
if (details.payload == null) return;
|
||||
|
||||
if (int.tryParse(details.payload!) case final rowId?) {
|
||||
if (navigatorKey.currentContext case final context?) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => NotificationsList(
|
||||
highlightedEvent: rowId,
|
||||
defaultToAllNotifications: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
navigatorKey.currentState?.popUntil((route) => route.isFirst);
|
||||
ref
|
||||
.read(NotificationRequestController.provider.notifier)
|
||||
.request(rowId);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -58,16 +52,10 @@ class NotificationController
|
|||
}
|
||||
|
||||
if (int.tryParse(call.arguments) case int rowId) {
|
||||
if (navigatorKey.currentContext case final context?) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => NotificationsList(
|
||||
highlightedEvent: rowId,
|
||||
defaultToAllNotifications: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
navigatorKey.currentState?.popUntil((route) => route.isFirst);
|
||||
ref
|
||||
.read(NotificationRequestController.provider.notifier)
|
||||
.request(rowId);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
14
lib/controllers/notification_request.dart
Normal file
14
lib/controllers/notification_request.dart
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
|
||||
class NotificationRequestController extends Notifier<int?> {
|
||||
@override
|
||||
int? build() => null;
|
||||
|
||||
void request(int rowId) => state = rowId;
|
||||
|
||||
void consume() => state = null;
|
||||
|
||||
static final provider = NotifierProvider<NotificationRequestController, int?>(
|
||||
NotificationRequestController.new,
|
||||
);
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import "package:m3e_buttons/m3e_buttons.dart";
|
|||
import "package:material_ui/material_ui.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:nexus/controllers/notification_request.dart";
|
||||
import "package:nexus/controllers/notifications.dart";
|
||||
import "package:nexus/helpers/extensions/focus_room.dart";
|
||||
import "package:nexus/models/event.dart";
|
||||
|
|
@ -14,11 +15,9 @@ import "package:nexus/widgets/loading.dart";
|
|||
import "package:nexus/widgets/renderers/event.dart";
|
||||
import "package:super_sliver_list/super_sliver_list.dart";
|
||||
|
||||
class const NotificationsList({
|
||||
final int? highlightedEvent,
|
||||
final bool defaultToAllNotifications = false,
|
||||
super.key,
|
||||
}) extends HookConsumerWidget implements NavPage {
|
||||
class const NotificationsList({super.key})
|
||||
extends HookConsumerWidget
|
||||
implements NavPage {
|
||||
@override
|
||||
String get title => "Notifications";
|
||||
|
||||
|
|
@ -41,9 +40,15 @@ class const NotificationsList({
|
|||
icon: Icon(Icons.notifications),
|
||||
): .notify,
|
||||
};
|
||||
final unreadTypeIndex = useState(defaultToAllNotifications ? 1 : 0);
|
||||
final allNotificationsIndex = options.values.toList().indexOf(.notify);
|
||||
|
||||
final highlightedId = useState(highlightedEvent);
|
||||
final initialRequest = useMemoized(
|
||||
() => ref.read(NotificationRequestController.provider),
|
||||
);
|
||||
final unreadTypeIndex = useState(
|
||||
initialRequest == null ? 0 : allNotificationsIndex,
|
||||
);
|
||||
final highlightedId = useState(initialRequest);
|
||||
final listController = useRef(ListController());
|
||||
final scrollController = useScrollController();
|
||||
|
||||
|
|
@ -54,21 +59,41 @@ class const NotificationsList({
|
|||
final notifications = ref.watch(provider);
|
||||
final notifier = ref.watch(provider.notifier);
|
||||
|
||||
useEffect(() {
|
||||
if (highlightedId.value == null) return null;
|
||||
Timer? timer;
|
||||
|
||||
void listener() =>
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
if (!listController.value.isAttached) return;
|
||||
|
||||
final notifications = await ref.watch(provider.future);
|
||||
|
||||
final index = notifications.indexWhere(
|
||||
(tuple) => tuple.$1 == highlightedId.value!,
|
||||
void consumeRequest() => Future.microtask(
|
||||
ref.read(NotificationRequestController.provider.notifier).consume,
|
||||
);
|
||||
|
||||
if (index == -1) return;
|
||||
useEffect(() {
|
||||
if (initialRequest != null) consumeRequest();
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
ref.listen(NotificationRequestController.provider, (_, next) {
|
||||
if (next == null) return;
|
||||
unreadTypeIndex.value = allNotificationsIndex;
|
||||
highlightedId.value = next;
|
||||
consumeRequest();
|
||||
});
|
||||
|
||||
useEffect(() {
|
||||
final target = highlightedId.value;
|
||||
if (target == null) return null;
|
||||
|
||||
Timer? timer;
|
||||
var done = false;
|
||||
|
||||
void listener() => WidgetsBinding.instance.addPostFrameCallback((
|
||||
_,
|
||||
) async {
|
||||
if (done || !listController.value.isAttached) return;
|
||||
|
||||
final notifications = await ref.read(provider.future);
|
||||
|
||||
final index = notifications.indexWhere((tuple) => tuple.$1 == target);
|
||||
|
||||
if (done || index == -1) return;
|
||||
done = true;
|
||||
listController.value.removeListener(listener);
|
||||
|
||||
listController.value.animateToItem(
|
||||
index: index,
|
||||
|
|
@ -80,12 +105,18 @@ class const NotificationsList({
|
|||
timer = Timer(.new(seconds: 1), () {
|
||||
highlightedId.value = null;
|
||||
});
|
||||
listController.value.removeListener(listener);
|
||||
});
|
||||
|
||||
listController.value.addListener(listener);
|
||||
return timer?.cancel;
|
||||
}, []);
|
||||
// The list may already be laid out, in which case no change is notified
|
||||
listener();
|
||||
|
||||
return () {
|
||||
done = true;
|
||||
listController.value.removeListener(listener);
|
||||
timer?.cancel();
|
||||
};
|
||||
}, [highlightedId.value]);
|
||||
|
||||
useEffect(() {
|
||||
Future<void> listener() async {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:nexus/controllers/init_complete.dart";
|
||||
import "package:nexus/controllers/key.dart";
|
||||
import "package:nexus/controllers/notification_request.dart";
|
||||
import "package:nexus/widgets/appbar.dart";
|
||||
import "package:nexus/widgets/sidebar.dart";
|
||||
import "package:nexus/widgets/room_chat/room_chat.dart";
|
||||
|
|
@ -9,17 +11,37 @@ import "package:nexus/widgets/loading.dart";
|
|||
|
||||
class const ChatPage({super.key}) extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) => LayoutBuilder(
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final scaffoldKey = useMemoized(GlobalKey<ScaffoldState>.new);
|
||||
final initComplete = ref.watch(InitCompleteController.provider);
|
||||
final notificationRequest = ref.watch(
|
||||
NotificationRequestController.provider,
|
||||
);
|
||||
|
||||
// On mobile, open the drawer so the sidebar can show the tapped notification
|
||||
useEffect(() {
|
||||
if (notificationRequest == null || !initComplete) return null;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final scaffold = scaffoldKey.currentState;
|
||||
if (scaffold != null && scaffold.hasDrawer && !scaffold.isDrawerOpen) {
|
||||
scaffold.openDrawer();
|
||||
}
|
||||
});
|
||||
return null;
|
||||
}, [notificationRequest, initComplete]);
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isDesktop = constraints.maxWidth > 650;
|
||||
final showMembersByDefault = constraints.maxWidth > 1000;
|
||||
final initComplete = ref.watch(InitCompleteController.provider);
|
||||
final roomId = ref
|
||||
.watch(KeyController.provider(KeyController.roomKey))
|
||||
.requireValue;
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
key: scaffoldKey,
|
||||
appBar: initComplete ? null : Appbar(),
|
||||
body: initComplete
|
||||
? Row(
|
||||
|
|
@ -46,4 +68,5 @@ class const ChatPage({super.key}) extends HookConsumerWidget {
|
|||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,29 @@
|
|||
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/notification_request.dart";
|
||||
import "package:nexus/models/nav_page.dart";
|
||||
import "package:nexus/widgets/invites_list.dart";
|
||||
import "package:nexus/widgets/notifications_list.dart";
|
||||
import "package:nexus/widgets/room_list.dart";
|
||||
|
||||
class const Sidebar({super.key}) extends HookWidget {
|
||||
class const Sidebar({super.key}) extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedIndex = useState(0);
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
const pages = <NavPage>[RoomList(), NotificationsList(), InvitesList()];
|
||||
final notificationsIndex = pages.indexWhere(
|
||||
(page) => page is NotificationsList,
|
||||
);
|
||||
|
||||
final selectedIndex = useState(
|
||||
ref.read(NotificationRequestController.provider) == null
|
||||
? 0
|
||||
: notificationsIndex,
|
||||
);
|
||||
|
||||
ref.listen(NotificationRequestController.provider, (_, next) {
|
||||
if (next != null) selectedIndex.value = notificationsIndex;
|
||||
});
|
||||
|
||||
return Drawer(
|
||||
width: 330,
|
||||
|
|
|
|||
Loading…
Reference in a new issue