Phase 3.1 WS2: FCM fallback Flutter + CC, unread badges, dynamic ping
- Control center: add mitra ping config UI (require ping toggle + interval)
- Mitra app StatusNotifier: honor require_ping and ping_interval_seconds
from API; skip heartbeat when ping not required
- Both apps: update notification services for FCM deep-linking
- mitra_app: handle chat_request (open_accept), session_closing
- client_app: handle session_closing, paired
- Unread badge providers:
- mitra_app: UnreadSessions provider (polls active-with-unread, badge
on active sessions button)
- client_app: UnreadCount provider (polls active-with-unread, badge
on _ActiveSessionCard)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
54
mitra_app/lib/core/chat/unread_notifier.dart
Normal file
54
mitra_app/lib/core/chat/unread_notifier.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import '../api/api_client_provider.dart';
|
||||
|
||||
part 'unread_notifier.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class UnreadSessions extends _$UnreadSessions {
|
||||
Timer? _pollTimer;
|
||||
|
||||
@override
|
||||
Map<String, int> build() {
|
||||
_startPolling();
|
||||
ref.onDispose(_stopPolling);
|
||||
return {};
|
||||
}
|
||||
|
||||
void _startPolling() {
|
||||
_stopPolling();
|
||||
_fetchUnreadCounts();
|
||||
_pollTimer = Timer.periodic(const Duration(seconds: 15), (_) {
|
||||
_fetchUnreadCounts();
|
||||
});
|
||||
}
|
||||
|
||||
void _stopPolling() {
|
||||
_pollTimer?.cancel();
|
||||
_pollTimer = null;
|
||||
}
|
||||
|
||||
Future<void> _fetchUnreadCounts() async {
|
||||
try {
|
||||
final apiClient = ref.read(apiClientProvider);
|
||||
final response = await apiClient.get('/api/mitra/chat-requests/sessions/active-with-unread');
|
||||
final sessions = response['data'] as List<dynamic>;
|
||||
final counts = <String, int>{};
|
||||
for (final s in sessions) {
|
||||
final id = s['id'] as String;
|
||||
final count = s['unread_count'] as int? ?? 0;
|
||||
if (count > 0) counts[id] = count;
|
||||
}
|
||||
state = counts;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
int get totalUnread => state.values.fold(0, (a, b) => a + b);
|
||||
|
||||
void markSessionRead(String sessionId) {
|
||||
state = {...state}..remove(sessionId);
|
||||
}
|
||||
|
||||
void refresh() => _fetchUnreadCounts();
|
||||
}
|
||||
26
mitra_app/lib/core/chat/unread_notifier.g.dart
Normal file
26
mitra_app/lib/core/chat/unread_notifier.g.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'unread_notifier.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$unreadSessionsHash() => r'd2ff837f1e781e6aa624b3d3ca2befb0d1d258e8';
|
||||
|
||||
/// See also [UnreadSessions].
|
||||
@ProviderFor(UnreadSessions)
|
||||
final unreadSessionsProvider =
|
||||
NotifierProvider<UnreadSessions, Map<String, int>>.internal(
|
||||
UnreadSessions.new,
|
||||
name: r'unreadSessionsProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$unreadSessionsHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$UnreadSessions = Notifier<Map<String, int>>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
Reference in New Issue
Block a user