105 lines
3.3 KiB
Dart
105 lines
3.3 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|
import 'package:http/http.dart' as http;
|
|
|
|
import '../local/models/app_settings.dart';
|
|
import 'notification_inbox_service.dart';
|
|
import 'notification_runtime_service.dart';
|
|
import 'notification_service.dart';
|
|
|
|
/// Pulls server-defined notification content and maps it to local inbox items.
|
|
class RemoteNotificationContentService {
|
|
RemoteNotificationContentService._();
|
|
static final RemoteNotificationContentService instance =
|
|
RemoteNotificationContentService._();
|
|
|
|
final NotificationInboxService _inbox = NotificationInboxService.instance;
|
|
final NotificationRuntimeService _runtime =
|
|
NotificationRuntimeService.instance;
|
|
|
|
Future<void> sync({
|
|
required AppSettings settings,
|
|
}) async {
|
|
if (!settings.inboxEnabled) return;
|
|
|
|
final endpoint = (dotenv.env['NOTIFICATION_FEED_URL'] ?? '').trim();
|
|
if (endpoint.isEmpty) return;
|
|
|
|
final now = DateTime.now();
|
|
final lastSync = _runtime.lastRemoteSyncAt();
|
|
if (lastSync != null &&
|
|
now.difference(lastSync) < const Duration(hours: 6)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
final response = await http.get(Uri.parse(endpoint));
|
|
if (response.statusCode < 200 || response.statusCode >= 300) return;
|
|
|
|
final decoded = json.decode(response.body);
|
|
final items = _extractItems(decoded);
|
|
if (items.isEmpty) return;
|
|
|
|
for (final raw in items) {
|
|
final id = (raw['id'] ?? '').toString().trim();
|
|
final title = (raw['title'] ?? '').toString().trim();
|
|
final body = (raw['body'] ?? '').toString().trim();
|
|
if (id.isEmpty || title.isEmpty || body.isEmpty) continue;
|
|
|
|
final deeplink = (raw['deeplink'] ?? '').toString().trim();
|
|
final type = (raw['type'] ?? 'content').toString().trim();
|
|
final expiresAt =
|
|
DateTime.tryParse((raw['expiresAt'] ?? '').toString().trim());
|
|
final isPinned = raw['isPinned'] == true;
|
|
final shouldPush = raw['push'] == true;
|
|
|
|
await _inbox.addItem(
|
|
title: title,
|
|
body: body,
|
|
type: type.isEmpty ? 'content' : type,
|
|
source: 'remote',
|
|
deeplink: deeplink.isEmpty ? null : deeplink,
|
|
dedupeKey: 'remote.$id',
|
|
expiresAt: expiresAt,
|
|
isPinned: isPinned,
|
|
meta: <String, dynamic>{'remoteId': id},
|
|
);
|
|
|
|
if (shouldPush && settings.alertsEnabled) {
|
|
final notif = NotificationService.instance;
|
|
await notif.showNonPrayerAlert(
|
|
settings: settings,
|
|
id: notif.nonPrayerNotificationId('remote.push.$id'),
|
|
title: title,
|
|
body: body,
|
|
payloadType: 'content',
|
|
silent: true,
|
|
);
|
|
}
|
|
}
|
|
|
|
await _runtime.setLastRemoteSyncAt(now);
|
|
} catch (_) {
|
|
// Non-fatal: remote feed is optional.
|
|
}
|
|
}
|
|
|
|
List<Map<String, dynamic>> _extractItems(dynamic decoded) {
|
|
if (decoded is List) {
|
|
return decoded.whereType<Map>().map(_toStringKeyedMap).toList();
|
|
}
|
|
if (decoded is Map) {
|
|
final list = decoded['items'];
|
|
if (list is List) {
|
|
return list.whereType<Map>().map(_toStringKeyedMap).toList();
|
|
}
|
|
}
|
|
return const <Map<String, dynamic>>[];
|
|
}
|
|
|
|
Map<String, dynamic> _toStringKeyedMap(Map raw) {
|
|
return raw.map((key, value) => MapEntry(key.toString(), value));
|
|
}
|
|
}
|