Files
jamshalat-diary/lib/data/services/remote_notification_content_service.dart
2026-03-18 00:07:10 +07:00

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));
}
}