Notifications page
Doesn't yet allow for jumping to events
This commit is contained in:
parent
9e7fcdbef7
commit
23e01c7a0d
12 changed files with 296 additions and 139 deletions
|
|
@ -25,6 +25,7 @@ import "package:nexus/models/paginate.dart";
|
||||||
import "package:nexus/models/requests/deregister_pusher.dart";
|
import "package:nexus/models/requests/deregister_pusher.dart";
|
||||||
import "package:nexus/models/requests/download_media.dart";
|
import "package:nexus/models/requests/download_media.dart";
|
||||||
import "package:nexus/models/requests/get_event.dart";
|
import "package:nexus/models/requests/get_event.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/join_room.dart";
|
import "package:nexus/models/requests/join_room.dart";
|
||||||
|
|
@ -272,6 +273,12 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
return .new(response?.map((event) => .fromJson(event)));
|
return .new(response?.map((event) => .fromJson(event)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<IList<Event>> getMentions(GetMentionsRequest request) async => .new(
|
||||||
|
((await _sendCommand("get_mentions", request.toJson())) as List).map(
|
||||||
|
(event) => .fromJson(event),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
Future<Event?> getEvent(GetEventRequest request) async {
|
Future<Event?> getEvent(GetEventRequest request) async {
|
||||||
final json = await _sendCommand("get_event", request.toJson());
|
final json = await _sendCommand("get_event", request.toJson());
|
||||||
return json == null ? null : .fromJson(json);
|
return json == null ? null : .fromJson(json);
|
||||||
|
|
|
||||||
120
lib/controllers/notification.dart
Normal file
120
lib/controllers/notification.dart
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
import "dart:io";
|
||||||
|
|
||||||
|
import "package:material_ui/material_ui.dart";
|
||||||
|
import "package:nexus/main.dart";
|
||||||
|
import "package:flutter_local_notifications/flutter_local_notifications.dart";
|
||||||
|
// ignore: implementation_imports
|
||||||
|
import "package:flutter_local_notifications_linux/src/model/hint.dart";
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/pages/notifications.dart";
|
||||||
|
|
||||||
|
class NotificationController
|
||||||
|
extends AsyncNotifier<FlutterLocalNotificationsPlugin> {
|
||||||
|
@override
|
||||||
|
Future<FlutterLocalNotificationsPlugin> build() async {
|
||||||
|
final notifications = FlutterLocalNotificationsPlugin();
|
||||||
|
|
||||||
|
final darwin = DarwinInitializationSettings();
|
||||||
|
|
||||||
|
await notifications.initialize(
|
||||||
|
settings: .new(
|
||||||
|
windows: .new(
|
||||||
|
appName: "Nexus",
|
||||||
|
appUserModelId: "nexus.federated.nexus",
|
||||||
|
guid: "dde78daf-130f-4e46-a80a-e31deeab45d7",
|
||||||
|
),
|
||||||
|
android: .new("ic_launcher_foreground"),
|
||||||
|
iOS: darwin,
|
||||||
|
macOS: darwin,
|
||||||
|
linux: .new(defaultActionName: "Open"),
|
||||||
|
),
|
||||||
|
onDidReceiveNotificationResponse: (details) {
|
||||||
|
if (details.payload case final eventId?) {
|
||||||
|
if (navigatorKey.currentContext case final context?) {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => NotificationsPage(eventId: eventId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return notifications;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> requestPermissions() async {
|
||||||
|
final controller = await future;
|
||||||
|
|
||||||
|
if (Platform.isIOS) {
|
||||||
|
return await controller
|
||||||
|
.resolvePlatformSpecificImplementation<
|
||||||
|
IOSFlutterLocalNotificationsPlugin
|
||||||
|
>()
|
||||||
|
?.requestPermissions(alert: true, badge: true, sound: true) ??
|
||||||
|
true;
|
||||||
|
} else if (Platform.isMacOS) {
|
||||||
|
return await controller
|
||||||
|
.resolvePlatformSpecificImplementation<
|
||||||
|
MacOSFlutterLocalNotificationsPlugin
|
||||||
|
>()
|
||||||
|
?.requestPermissions(alert: true, badge: true, sound: true) ??
|
||||||
|
true;
|
||||||
|
} else if (Platform.isAndroid) {
|
||||||
|
return await controller
|
||||||
|
.resolvePlatformSpecificImplementation<
|
||||||
|
AndroidFlutterLocalNotificationsPlugin
|
||||||
|
>()
|
||||||
|
?.requestNotificationsPermission() ??
|
||||||
|
true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> send({
|
||||||
|
required int id,
|
||||||
|
required String title,
|
||||||
|
String? body,
|
||||||
|
File? icon,
|
||||||
|
String? payload,
|
||||||
|
}) async {
|
||||||
|
final notificationDetails = NotificationDetails(
|
||||||
|
android: .new(
|
||||||
|
"messages",
|
||||||
|
"Messages",
|
||||||
|
largeIcon: icon == null ? null : FilePathAndroidBitmap(icon.path),
|
||||||
|
),
|
||||||
|
linux: .new(
|
||||||
|
customHints: icon == null
|
||||||
|
? null
|
||||||
|
: [
|
||||||
|
.new(
|
||||||
|
name: "image-path",
|
||||||
|
value: LinuxHintStringValue(icon.path),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// TODO: See if icons can be added to iOS, macOS, and Windows notifications (#68)
|
||||||
|
iOS: .new(),
|
||||||
|
macOS: .new(),
|
||||||
|
windows: .new(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final controller = await future;
|
||||||
|
await controller.show(
|
||||||
|
id: id,
|
||||||
|
title: title,
|
||||||
|
body: body,
|
||||||
|
notificationDetails: notificationDetails,
|
||||||
|
payload: payload,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static final provider =
|
||||||
|
AsyncNotifierProvider<
|
||||||
|
NotificationController,
|
||||||
|
FlutterLocalNotificationsPlugin
|
||||||
|
>(NotificationController.new);
|
||||||
|
}
|
||||||
|
|
@ -1,120 +1,59 @@
|
||||||
import "dart:io";
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
|
||||||
import "package:material_ui/material_ui.dart";
|
|
||||||
import "package:nexus/main.dart";
|
|
||||||
import "package:flutter_local_notifications/flutter_local_notifications.dart";
|
|
||||||
// ignore: implementation_imports
|
|
||||||
import "package:flutter_local_notifications_linux/src/model/hint.dart";
|
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/pages/notifications.dart";
|
import "package:nexus/controllers/client.dart";
|
||||||
|
import "package:nexus/models/event.dart";
|
||||||
|
|
||||||
|
typedef NotificationsRequest = (UnreadType? unreadType, String? roomId);
|
||||||
|
|
||||||
|
class NotificationsController([final NotificationsRequest? request])
|
||||||
|
extends AsyncNotifier<IList<Event>> {
|
||||||
|
static const limit = 20;
|
||||||
|
|
||||||
class NotificationsController
|
|
||||||
extends AsyncNotifier<FlutterLocalNotificationsPlugin> {
|
|
||||||
@override
|
@override
|
||||||
Future<FlutterLocalNotificationsPlugin> build() async {
|
Future<IList<Event>> build() async {
|
||||||
final notifications = FlutterLocalNotificationsPlugin();
|
final client = ref.watch(ClientController.provider.notifier);
|
||||||
|
|
||||||
final darwin = DarwinInitializationSettings();
|
final (unreadType, roomId) = request ?? (null, null);
|
||||||
|
|
||||||
await notifications.initialize(
|
final mentions = await client.getMentions(
|
||||||
settings: .new(
|
.new(
|
||||||
windows: .new(
|
maxTimestamp: .now(),
|
||||||
appName: "Nexus",
|
unreadType: unreadType ?? .highlight,
|
||||||
appUserModelId: "nexus.federated.nexus",
|
limit: limit,
|
||||||
guid: "dde78daf-130f-4e46-a80a-e31deeab45d7",
|
roomId: roomId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return mentions;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> loadOlder() async {
|
||||||
|
final currentNotifications = await future;
|
||||||
|
state = .loading();
|
||||||
|
state = await .guard(() async {
|
||||||
|
final lastTs = currentNotifications.lastOrNull?.timestamp;
|
||||||
|
if (lastTs == null) return const .empty();
|
||||||
|
|
||||||
|
final client = ref.watch(ClientController.provider.notifier);
|
||||||
|
final (unreadType, roomId) = request ?? (null, null);
|
||||||
|
|
||||||
|
final newNotifications = await client.getMentions(
|
||||||
|
.new(
|
||||||
|
maxTimestamp: lastTs,
|
||||||
|
unreadType: unreadType ?? .highlight,
|
||||||
|
limit: limit,
|
||||||
|
roomId: roomId,
|
||||||
),
|
),
|
||||||
android: .new("ic_launcher_foreground"),
|
);
|
||||||
iOS: darwin,
|
|
||||||
macOS: darwin,
|
|
||||||
linux: .new(defaultActionName: "Open"),
|
|
||||||
),
|
|
||||||
onDidReceiveNotificationResponse: (details) {
|
|
||||||
if (details.payload case final eventId?) {
|
|
||||||
if (navigatorKey.currentContext case final context?) {
|
|
||||||
Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (_) => NotificationsPage(eventId: eventId),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return notifications;
|
return currentNotifications.addAll(newNotifications);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> requestPermissions() async {
|
static final provider = AsyncNotifierProvider.family
|
||||||
final controller = await future;
|
.autoDispose<
|
||||||
|
|
||||||
if (Platform.isIOS) {
|
|
||||||
return await controller
|
|
||||||
.resolvePlatformSpecificImplementation<
|
|
||||||
IOSFlutterLocalNotificationsPlugin
|
|
||||||
>()
|
|
||||||
?.requestPermissions(alert: true, badge: true, sound: true) ??
|
|
||||||
true;
|
|
||||||
} else if (Platform.isMacOS) {
|
|
||||||
return await controller
|
|
||||||
.resolvePlatformSpecificImplementation<
|
|
||||||
MacOSFlutterLocalNotificationsPlugin
|
|
||||||
>()
|
|
||||||
?.requestPermissions(alert: true, badge: true, sound: true) ??
|
|
||||||
true;
|
|
||||||
} else if (Platform.isAndroid) {
|
|
||||||
return await controller
|
|
||||||
.resolvePlatformSpecificImplementation<
|
|
||||||
AndroidFlutterLocalNotificationsPlugin
|
|
||||||
>()
|
|
||||||
?.requestNotificationsPermission() ??
|
|
||||||
true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> send({
|
|
||||||
required int id,
|
|
||||||
required String title,
|
|
||||||
String? body,
|
|
||||||
File? icon,
|
|
||||||
String? payload,
|
|
||||||
}) async {
|
|
||||||
final notificationDetails = NotificationDetails(
|
|
||||||
android: .new(
|
|
||||||
"messages",
|
|
||||||
"Messages",
|
|
||||||
largeIcon: icon == null ? null : FilePathAndroidBitmap(icon.path),
|
|
||||||
),
|
|
||||||
linux: .new(
|
|
||||||
customHints: icon == null
|
|
||||||
? null
|
|
||||||
: [
|
|
||||||
.new(
|
|
||||||
name: "image-path",
|
|
||||||
value: LinuxHintStringValue(icon.path),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
// TODO: See if icons can be added to iOS, macOS, and Windows notifications (#68)
|
|
||||||
iOS: .new(),
|
|
||||||
macOS: .new(),
|
|
||||||
windows: .new(),
|
|
||||||
);
|
|
||||||
|
|
||||||
final controller = await future;
|
|
||||||
await controller.show(
|
|
||||||
id: id,
|
|
||||||
title: title,
|
|
||||||
body: body,
|
|
||||||
notificationDetails: notificationDetails,
|
|
||||||
payload: payload,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static final provider =
|
|
||||||
AsyncNotifierProvider<
|
|
||||||
NotificationsController,
|
NotificationsController,
|
||||||
FlutterLocalNotificationsPlugin
|
IList<Event>,
|
||||||
|
NotificationsRequest?
|
||||||
>(NotificationsController.new);
|
>(NotificationsController.new);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ class RoomChatController(final String roomId)
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<bool> loadOlder() async {
|
Future<bool> loadOlder() async {
|
||||||
state = AsyncLoading();
|
state = .loading();
|
||||||
final timelineKeys = ref
|
final timelineKeys = ref
|
||||||
.read(RoomsController.provider.select((value) => value[roomId]))
|
.read(RoomsController.provider.select((value) => value[roomId]))
|
||||||
?.timeline
|
?.timeline
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import "package:m3e_buttons/m3e_buttons.dart";
|
||||||
import "package:nexus/controllers/account_data.dart";
|
import "package:nexus/controllers/account_data.dart";
|
||||||
import "package:nexus/controllers/client.dart";
|
import "package:nexus/controllers/client.dart";
|
||||||
import "package:nexus/controllers/client_state.dart";
|
import "package:nexus/controllers/client_state.dart";
|
||||||
import "package:nexus/controllers/notifications.dart";
|
import "package:nexus/controllers/notification.dart";
|
||||||
import "package:nexus/controllers/settings.dart";
|
import "package:nexus/controllers/settings.dart";
|
||||||
import "package:nexus/controllers/unified_push.dart";
|
import "package:nexus/controllers/unified_push.dart";
|
||||||
import "package:nexus/controllers/spec_versions.dart";
|
import "package:nexus/controllers/spec_versions.dart";
|
||||||
|
|
@ -143,7 +143,7 @@ class SettingsSectionsController
|
||||||
if (value) {
|
if (value) {
|
||||||
if (await ref
|
if (await ref
|
||||||
.watch(
|
.watch(
|
||||||
NotificationsController
|
NotificationController
|
||||||
.provider
|
.provider
|
||||||
.notifier,
|
.notifier,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import "package:flutter/foundation.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:intl/intl.dart";
|
import "package:intl/intl.dart";
|
||||||
import "package:nexus/controllers/key.dart";
|
import "package:nexus/controllers/key.dart";
|
||||||
import "package:nexus/controllers/notifications.dart";
|
import "package:nexus/controllers/notification.dart";
|
||||||
import "package:nexus/controllers/push_key.dart";
|
import "package:nexus/controllers/push_key.dart";
|
||||||
import "package:nexus/controllers/rooms.dart";
|
import "package:nexus/controllers/rooms.dart";
|
||||||
import "package:nexus/main.dart";
|
import "package:nexus/main.dart";
|
||||||
|
|
@ -61,7 +61,7 @@ class UnifiedPushController extends AsyncNotifier<bool> {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (event == null ||
|
if (event == null ||
|
||||||
event.unreadType?.shouldNotify != true ||
|
event.unreadType?.shouldNotify() != true ||
|
||||||
(!isInBackground &&
|
(!isInBackground &&
|
||||||
await windowManager.isFocused().onError((_, _) => false) &&
|
await windowManager.isFocused().onError((_, _) => false) &&
|
||||||
ref.watch(KeyController.provider(KeyController.roomKey)) ==
|
ref.watch(KeyController.provider(KeyController.roomKey)) ==
|
||||||
|
|
@ -81,7 +81,7 @@ class UnifiedPushController extends AsyncNotifier<bool> {
|
||||||
.downloadMedia(.new(mxc: avatar, isAvatar: true));
|
.downloadMedia(.new(mxc: avatar, isAvatar: true));
|
||||||
|
|
||||||
await ref
|
await ref
|
||||||
.read(NotificationsController.provider.notifier)
|
.read(NotificationController.provider.notifier)
|
||||||
.send(
|
.send(
|
||||||
id: event.eventId.hashCode & 0x7fffffff,
|
id: event.eventId.hashCode & 0x7fffffff,
|
||||||
title: room?.metadata?.name ?? "New Event",
|
title: room?.metadata?.name ?? "New Event",
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import "package:media_kit/media_kit.dart";
|
||||||
import "package:nexus/controllers/client.dart";
|
import "package:nexus/controllers/client.dart";
|
||||||
import "package:nexus/controllers/client_state.dart";
|
import "package:nexus/controllers/client_state.dart";
|
||||||
import "package:nexus/controllers/multi_provider.dart";
|
import "package:nexus/controllers/multi_provider.dart";
|
||||||
import "package:nexus/controllers/notifications.dart";
|
import "package:nexus/controllers/notification.dart";
|
||||||
import "package:nexus/controllers/settings.dart";
|
import "package:nexus/controllers/settings.dart";
|
||||||
import "package:nexus/controllers/shared_prefs.dart";
|
import "package:nexus/controllers/shared_prefs.dart";
|
||||||
import "package:nexus/controllers/unified_push.dart";
|
import "package:nexus/controllers/unified_push.dart";
|
||||||
|
|
@ -147,7 +147,7 @@ class const App(final bool isInBackground, {super.key})
|
||||||
IListConst([
|
IListConst([
|
||||||
SharedPrefsController.provider,
|
SharedPrefsController.provider,
|
||||||
ClientController.provider,
|
ClientController.provider,
|
||||||
NotificationsController.provider,
|
NotificationController.provider,
|
||||||
UnifiedPushController.provider,
|
UnifiedPushController.provider,
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ class const Event({
|
||||||
final String? sendError,
|
final String? sendError,
|
||||||
final IMap<String, int> reactions = const IMap.empty(),
|
final IMap<String, int> reactions = const IMap.empty(),
|
||||||
@JsonKey(name: "last_edit_rowid") final int lastEditRowId = 0,
|
@JsonKey(name: "last_edit_rowid") final int lastEditRowId = 0,
|
||||||
@UnreadTypeConverter() final UnreadType? unreadType,
|
final UnreadType? unreadType,
|
||||||
final Profile? pmp,
|
final Profile? pmp,
|
||||||
required final Content content,
|
required final Content content,
|
||||||
required final Content? previousContent,
|
required final Content? previousContent,
|
||||||
|
|
@ -91,22 +91,10 @@ class const LocalContent({
|
||||||
_$LocalContentFromJson(json);
|
_$LocalContentFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
class UnreadTypeConverter implements JsonConverter<UnreadType?, int?> {
|
@Freezed(toJson: false, fromJson: false)
|
||||||
const UnreadTypeConverter();
|
class const UnreadType(final int value) with _$UnreadType {
|
||||||
|
factory UnreadType.fromJson(int json) => UnreadType(json);
|
||||||
@override
|
int toJson() => value;
|
||||||
UnreadType? fromJson(int? json) => json == null ? null : UnreadType(json);
|
|
||||||
|
|
||||||
@override
|
|
||||||
int? toJson(UnreadType? object) => object?.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// I think this is correct but I'm not sure, its some type of bitmask.
|
|
||||||
@immutable
|
|
||||||
class UnreadType {
|
|
||||||
final int value;
|
|
||||||
|
|
||||||
const UnreadType(this.value);
|
|
||||||
|
|
||||||
static const none = UnreadType(0);
|
static const none = UnreadType(0);
|
||||||
static const normal = UnreadType(1);
|
static const normal = UnreadType(1);
|
||||||
|
|
@ -114,9 +102,9 @@ class UnreadType {
|
||||||
static const highlight = UnreadType(4);
|
static const highlight = UnreadType(4);
|
||||||
static const sound = UnreadType(8);
|
static const sound = UnreadType(8);
|
||||||
|
|
||||||
bool get isNone => value == 0;
|
bool isNone() => value == 0;
|
||||||
bool get isNormal => (value & 1) != 0;
|
bool isNormal() => (value & 1) != 0;
|
||||||
bool get shouldNotify => (value & 2) != 0;
|
bool shouldNotify() => (value & 2) != 0;
|
||||||
bool get isHighlighted => (value & 4) != 0;
|
bool isHighlighted() => (value & 4) != 0;
|
||||||
bool get playsSound => (value & 8) != 0;
|
bool playsSound() => (value & 8) != 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
19
lib/models/requests/get_mentions.dart
Normal file
19
lib/models/requests/get_mentions.dart
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
|
import "package:nexus/models/epoch_date_time_converter.dart";
|
||||||
|
import "package:nexus/models/event.dart";
|
||||||
|
part "get_mentions.freezed.dart";
|
||||||
|
part "get_mentions.g.dart";
|
||||||
|
|
||||||
|
@Freezed(toJson: false, fromJson: false)
|
||||||
|
@JsonSerializable()
|
||||||
|
class GetMentionsRequest({
|
||||||
|
@EpochDateTimeConverter() required final DateTime maxTimestamp,
|
||||||
|
@JsonKey(name: "type") required final UnreadType unreadType,
|
||||||
|
required final int limit,
|
||||||
|
final String? roomId,
|
||||||
|
}) with _$GetMentionsRequest {
|
||||||
|
Map<String, Object?> toJson() => _$GetMentionsRequestToJson(this);
|
||||||
|
|
||||||
|
factory GetMentionsRequest.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$GetMentionsRequestFromJson(json);
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,86 @@
|
||||||
import "package:material_ui/material_ui.dart";
|
import "package:material_ui/material_ui.dart";
|
||||||
|
import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/notifications.dart";
|
||||||
import "package:nexus/widgets/appbar.dart";
|
import "package:nexus/widgets/appbar.dart";
|
||||||
|
import "package:nexus/widgets/error_dialog.dart";
|
||||||
|
import "package:nexus/widgets/highlight_wrapper.dart";
|
||||||
|
import "package:nexus/widgets/loading.dart";
|
||||||
|
import "package:nexus/widgets/renderers/event.dart";
|
||||||
|
import "package:super_sliver_list/super_sliver_list.dart";
|
||||||
|
|
||||||
class const NotificationsPage({final String? eventId, super.key})
|
class const NotificationsPage({final String? eventId, super.key})
|
||||||
extends HookConsumerWidget {
|
extends HookConsumerWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final provider = NotificationsController.provider(null);
|
||||||
|
final notifications = ref.watch(provider);
|
||||||
|
final notifier = ref.watch(provider.notifier);
|
||||||
|
|
||||||
|
final scrollController = useScrollController();
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
Future<void> listener() async {
|
||||||
|
if (!scrollController.hasClients || notifications.isLoading) return;
|
||||||
|
|
||||||
|
if (scrollController.position.pixels >=
|
||||||
|
scrollController.position.maxScrollExtent) {
|
||||||
|
await notifier.loadOlder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollController.addListener(listener);
|
||||||
|
return () => scrollController.removeListener(listener);
|
||||||
|
}, [scrollController, notifications]);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: Appbar(title: Text("Notifications")),
|
appBar: Appbar(title: Text("Notifications")),
|
||||||
body: Placeholder(),
|
body: Column(
|
||||||
|
children: [
|
||||||
|
if (notifications is AsyncLoading && notifications.value != null)
|
||||||
|
const LinearProgressIndicator(minHeight: 2),
|
||||||
|
Expanded(
|
||||||
|
child: switch (notifications) {
|
||||||
|
AsyncData(:final value) || AsyncLoading(:final value?) =>
|
||||||
|
value.isEmpty
|
||||||
|
? Center(
|
||||||
|
child: Text(
|
||||||
|
"No notifications yet",
|
||||||
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: SuperListView.builder(
|
||||||
|
controller: scrollController,
|
||||||
|
itemCount: value.length,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
reverse: true,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final event = value[index];
|
||||||
|
final isHighlighted = event.eventId == eventId;
|
||||||
|
|
||||||
|
return HighlightWrapper(
|
||||||
|
InkWell(
|
||||||
|
onTap: () {
|
||||||
|
//TODO
|
||||||
|
},
|
||||||
|
child: EventRenderer(event),
|
||||||
|
),
|
||||||
|
isHighlighted: isHighlighted,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
AsyncLoading() => const Loading(),
|
||||||
|
AsyncError(:final error, :final stackTrace) => ErrorDialog(
|
||||||
|
error,
|
||||||
|
stackTrace,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,10 +78,12 @@ final class const RoomChat({
|
||||||
final hasMore = useState<bool>(true);
|
final hasMore = useState<bool>(true);
|
||||||
|
|
||||||
Future<void> jumpToId(String eventId) async {
|
Future<void> jumpToId(String eventId) async {
|
||||||
final index = controllerData.value?.indexWhere(
|
final index =
|
||||||
(element) => element.eventId == eventId,
|
controllerData.value?.indexWhere(
|
||||||
);
|
(element) => element.eventId == eventId,
|
||||||
if (index == null) return;
|
) ??
|
||||||
|
-1;
|
||||||
|
if (index == -1) return;
|
||||||
|
|
||||||
listController.value.animateToItem(
|
listController.value.animateToItem(
|
||||||
index: index,
|
index: index,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import "package:navigation_rail_m3e/navigation_rail_m3e.dart";
|
||||||
import "package:nexus/controllers/key.dart";
|
import "package:nexus/controllers/key.dart";
|
||||||
import "package:nexus/controllers/spaces.dart";
|
import "package:nexus/controllers/spaces.dart";
|
||||||
import "package:nexus/models/room.dart";
|
import "package:nexus/models/room.dart";
|
||||||
|
import "package:nexus/pages/notifications.dart";
|
||||||
import "package:nexus/pages/settings.dart";
|
import "package:nexus/pages/settings.dart";
|
||||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||||
import "package:nexus/widgets/divider_widget.dart";
|
import "package:nexus/widgets/divider_widget.dart";
|
||||||
|
|
@ -189,6 +190,15 @@ class const Sidebar({required final bool isDesktop, super.key})
|
||||||
onPressed: null,
|
onPressed: null,
|
||||||
icon: Icon(Icons.explore),
|
icon: Icon(Icons.explore),
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: "Open notifications",
|
||||||
|
onPressed: () => Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => NotificationsPage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
icon: Icon(Icons.notifications),
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: "Open settings",
|
tooltip: "Open settings",
|
||||||
onPressed: () => showDialog(
|
onPressed: () => showDialog(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue