simplified code across the app and consolidated duplicated helpers
This commit is contained in:
@@ -6,13 +6,20 @@ import 'package:localstore/localstore.dart';
|
||||
/// notification, small enough to stay cheap in the background isolate.
|
||||
const int kChatThreadCap = 15;
|
||||
|
||||
/// Canonical per-chat notification tag (kept in sync with
|
||||
/// `NotificationTasks.chatTag`, which delegates here).
|
||||
/// Canonical per-chat notification tag.
|
||||
String chatNotificationTag(String chatToken) => 'talk_$chatToken';
|
||||
|
||||
/// Deterministic non-negative 31-bit notification id per chat, so every
|
||||
/// message of the same conversation updates ONE stacked notification instead
|
||||
/// of adding a new card per push.
|
||||
final _fileSafeToken = RegExp(r'^[A-Za-z0-9_-]+$');
|
||||
|
||||
/// File-/doc-safe name for a chat token: normally URL-safe and used verbatim,
|
||||
/// anything else is base64url-encoded so exotic ids can't escape a directory.
|
||||
String fileSafeToken(String chatToken) => _fileSafeToken.hasMatch(chatToken)
|
||||
? chatToken
|
||||
: base64Url.encode(utf8.encode(chatToken)).replaceAll('=', '');
|
||||
|
||||
int stableChatNotificationId(String chatToken) {
|
||||
var hash = 0;
|
||||
for (final unit in chatToken.codeUnits) {
|
||||
@@ -128,12 +135,8 @@ class ChatThreadStore {
|
||||
|
||||
ChatThreadStore({Localstore? db}) : _db = db ?? Localstore.instance;
|
||||
|
||||
/// Localstore doc ids become file names — tokens are normally URL-safe and
|
||||
/// used verbatim, anything else is base64url-encoded.
|
||||
static String docIdForToken(String chatToken) =>
|
||||
RegExp(r'^[A-Za-z0-9_-]+$').hasMatch(chatToken)
|
||||
? chatToken
|
||||
: base64Url.encode(utf8.encode(chatToken)).replaceAll('=', '');
|
||||
/// Localstore doc ids become file names.
|
||||
static String docIdForToken(String chatToken) => fileSafeToken(chatToken);
|
||||
|
||||
Future<List<ThreadMessage>> messages(String chatToken) async {
|
||||
final data = await _doc(chatToken).get();
|
||||
@@ -145,16 +148,6 @@ class ChatThreadStore {
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Appends [message] and returns the updated (capped) history.
|
||||
Future<List<ThreadMessage>> append(
|
||||
String chatToken,
|
||||
ThreadMessage message,
|
||||
) async {
|
||||
final updated = appendThreadMessage(await messages(chatToken), message);
|
||||
await _write(chatToken, updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
/// Appends [message], but restarts the thread first when the chat's previous
|
||||
/// notification is no longer active ([isActive] == false — dismissed/read).
|
||||
/// See [threadAfterIncoming] for the tri-state semantics.
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:ui';
|
||||
|
||||
@@ -13,6 +12,7 @@ import '../session/session_manager.dart';
|
||||
import 'chat_thread_store.dart';
|
||||
import 'nid_store.dart';
|
||||
import 'push_renderer.dart';
|
||||
import 'push_target.dart';
|
||||
|
||||
/// Notification action identifiers shared between the renderer (which attaches
|
||||
/// the actions) and the response handlers (which dispatch them).
|
||||
@@ -93,8 +93,7 @@ class PushActions {
|
||||
// the mark-read action does. Best effort: a failure here must not
|
||||
// fail the already-delivered reply.
|
||||
await markRead(chatToken);
|
||||
}
|
||||
if (!result.ok) {
|
||||
} else {
|
||||
// Never swallow the typed message: surface the failure (and the
|
||||
// undelivered text) as its own notification. The nid mapping stays
|
||||
// alive — the thread notification is still in the tray.
|
||||
@@ -287,14 +286,6 @@ class PushActions {
|
||||
return raw == null ? null : int.tryParse(raw);
|
||||
}
|
||||
|
||||
static String? _payloadField(String? payload, String key) {
|
||||
if (payload == null || payload.isEmpty) return null;
|
||||
try {
|
||||
final map = jsonDecode(payload) as Map<String, dynamic>;
|
||||
final value = map[key];
|
||||
return value?.toString();
|
||||
} on Object {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
static String? _payloadField(String? payload, String key) =>
|
||||
decodeNotificationPayload(payload)?[key]?.toString();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:http/http.dart' as http;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../api/marianumcloud/nextcloud_ocs.dart';
|
||||
import 'chat_thread_store.dart';
|
||||
|
||||
/// Result of an avatar lookup at render time: [icon] when bytes are available
|
||||
/// within the fetch budget, otherwise [late] carries the still-running fetch
|
||||
@@ -102,13 +103,7 @@ class PushAvatarStore {
|
||||
},
|
||||
);
|
||||
|
||||
/// File-safe cache name for a chat token. Tokens are normally URL-safe
|
||||
/// already and used verbatim; anything else is base64url-encoded so exotic
|
||||
/// ids can never escape the cache directory.
|
||||
static String fileNameForToken(String token) =>
|
||||
RegExp(r'^[A-Za-z0-9_-]+$').hasMatch(token)
|
||||
? token
|
||||
: base64Url.encode(utf8.encode(token)).replaceAll('=', '');
|
||||
static String fileNameForToken(String token) => fileSafeToken(token);
|
||||
|
||||
/// Whether a cache file written at [modified] is still fresh at [now].
|
||||
static bool isFresh(DateTime modified, DateTime now) =>
|
||||
|
||||
@@ -101,7 +101,6 @@ class PushMessageHandler {
|
||||
case PushKind.connect:
|
||||
await _handleConnect(
|
||||
message,
|
||||
foreground: foreground,
|
||||
notificationsEnabled: notificationsEnabled,
|
||||
);
|
||||
break;
|
||||
@@ -139,7 +138,6 @@ class PushMessageHandler {
|
||||
|
||||
Future<void> _handleConnect(
|
||||
RemoteMessage message, {
|
||||
required bool foreground,
|
||||
required bool notificationsEnabled,
|
||||
}) async {
|
||||
// Connect pushes carry no silent side effects, so nothing to do when the
|
||||
|
||||
@@ -258,10 +258,9 @@ class PushRegistration {
|
||||
/// Flutter engine. Best-effort — a failure here must not abort registration.
|
||||
Future<void> _persistNativeAuthContext() async {
|
||||
try {
|
||||
final endpoint = EndpointData().nextcloud();
|
||||
await _store.saveNativeAuthContext(
|
||||
username: SessionManager().requireNextcloud().username,
|
||||
baseUrl: 'https://${endpoint.full()}',
|
||||
baseUrl: currentNcBaseUrl,
|
||||
);
|
||||
} on Object catch (e) {
|
||||
log('Push: could not persist native auth context: $e');
|
||||
|
||||
+26
-23
@@ -203,16 +203,10 @@ class PushRenderer {
|
||||
messages: styleMessages,
|
||||
);
|
||||
|
||||
final androidDetails = AndroidNotificationDetails(
|
||||
talkChannelId,
|
||||
talkChannelName,
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
category: AndroidNotificationCategory.message,
|
||||
color: _accentColor,
|
||||
final androidDetails = _talkAndroidDetails(
|
||||
tag: tag,
|
||||
style: messagingStyle,
|
||||
silent: !alert,
|
||||
styleInformation: messagingStyle,
|
||||
actions: _talkActions,
|
||||
);
|
||||
|
||||
@@ -247,17 +241,11 @@ class PushRenderer {
|
||||
title: senderName,
|
||||
body: messageText,
|
||||
notificationDetails: NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
talkChannelId,
|
||||
talkChannelName,
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
category: AndroidNotificationCategory.message,
|
||||
color: _accentColor,
|
||||
android: _talkAndroidDetails(
|
||||
tag: tag,
|
||||
// Single sender ⇒ no conversationTitle (1:1 convention): the sender
|
||||
// Person is the header, a title would repeat the name per line.
|
||||
styleInformation: MessagingStyleInformation(
|
||||
style: MessagingStyleInformation(
|
||||
const Person(key: 'self', name: 'Ich'),
|
||||
groupConversation: false,
|
||||
messages: [
|
||||
@@ -271,6 +259,25 @@ class PushRenderer {
|
||||
);
|
||||
}
|
||||
|
||||
static AndroidNotificationDetails _talkAndroidDetails({
|
||||
required String tag,
|
||||
required StyleInformation style,
|
||||
AndroidNotificationCategory? category = AndroidNotificationCategory.message,
|
||||
bool silent = false,
|
||||
List<AndroidNotificationAction>? actions,
|
||||
}) => AndroidNotificationDetails(
|
||||
talkChannelId,
|
||||
talkChannelName,
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
category: category,
|
||||
color: _accentColor,
|
||||
tag: tag,
|
||||
silent: silent,
|
||||
styleInformation: style,
|
||||
actions: actions,
|
||||
);
|
||||
|
||||
// Both actions keep cancelNotification: true (the default): the plugin's
|
||||
// Java receiver then removes the card NATIVELY the moment the action fires.
|
||||
// The reply action must not rely on our Dart-side cancel instead — MIUI/
|
||||
@@ -306,14 +313,10 @@ class PushRenderer {
|
||||
title: title,
|
||||
body: body,
|
||||
notificationDetails: NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
talkChannelId,
|
||||
talkChannelName,
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
color: _accentColor,
|
||||
android: _talkAndroidDetails(
|
||||
tag: 'talk_error_$chatToken',
|
||||
styleInformation: BigTextStyleInformation(body),
|
||||
style: BigTextStyleInformation(body),
|
||||
category: null,
|
||||
),
|
||||
iOS: const DarwinNotificationDetails(),
|
||||
),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -25,7 +24,7 @@ class PushTapRouter {
|
||||
PushActions.handleBackgroundResponse(response);
|
||||
return;
|
||||
}
|
||||
final map = _payloadMap(response.payload);
|
||||
final map = decodeNotificationPayload(response.payload);
|
||||
if (map == null) return;
|
||||
final target = resolvePushTarget(map);
|
||||
if (target != null) pendingTarget.value = target;
|
||||
@@ -50,13 +49,4 @@ class PushTapRouter {
|
||||
log('Reading the notification launch details failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static Map<String, dynamic>? _payloadMap(String? payload) {
|
||||
if (payload == null || payload.isEmpty) return null;
|
||||
try {
|
||||
return jsonDecode(payload) as Map<String, dynamic>;
|
||||
} on Object {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
import 'dart:convert';
|
||||
|
||||
/// Decodes the JSON payload of a locally rendered notification; null when
|
||||
/// absent or malformed.
|
||||
Map<String, dynamic>? decodeNotificationPayload(String? payload) {
|
||||
if (payload == null || payload.isEmpty) return null;
|
||||
try {
|
||||
return jsonDecode(payload) as Map<String, dynamic>;
|
||||
} on Object {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a tapped notification leads.
|
||||
sealed class PushTarget {
|
||||
const PushTarget();
|
||||
|
||||
Reference in New Issue
Block a user