implemented chat long-polling and optimistic updates, centralized notification management, optimized avatar caching
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter_app_badge/flutter_app_badge.dart';
|
||||
|
||||
import '../../../../../api/errors/error_mapper.dart';
|
||||
import '../../../../../api/marianumcloud/talk/chat/get_chat_response.dart';
|
||||
import '../../../../../api/marianumcloud/talk/room/get_room_response.dart';
|
||||
import '../../../infrastructure/loadable_state/loading_error.dart';
|
||||
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart';
|
||||
@@ -15,6 +17,8 @@ class ChatListBloc
|
||||
extends
|
||||
LoadableHydratedBloc<ChatListEvent, ChatListState, ChatListRepository> {
|
||||
bool _forceRenew = false;
|
||||
Timer? _autoRefreshTimer;
|
||||
Duration? _autoRefreshInterval;
|
||||
|
||||
@override
|
||||
void retry() {
|
||||
@@ -22,6 +26,27 @@ class ChatListBloc
|
||||
super.retry();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_autoRefreshTimer?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
/// Sets (or clears) the recurring background refresh. Silent so the
|
||||
/// loading bar doesn't blink several times a minute; pull-to-refresh
|
||||
/// and tab-activation refreshes are non-silent for explicit feedback.
|
||||
void setAutoRefreshInterval(Duration? interval) {
|
||||
if (interval == _autoRefreshInterval) return;
|
||||
_autoRefreshInterval = interval;
|
||||
_autoRefreshTimer?.cancel();
|
||||
_autoRefreshTimer = null;
|
||||
if (interval == null) return;
|
||||
_autoRefreshTimer = Timer.periodic(interval, (_) {
|
||||
if (isClosed) return;
|
||||
refresh(silent: true);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
ChatListRepository repository() => ChatListRepository();
|
||||
|
||||
@@ -51,8 +76,8 @@ class ChatListBloc
|
||||
if (capturedError != null) throw capturedError!;
|
||||
}
|
||||
|
||||
Future<void> refresh({bool renew = true}) async {
|
||||
add(RefetchStarted<ChatListState>());
|
||||
Future<void> refresh({bool renew = true, bool silent = false}) async {
|
||||
if (!silent) add(RefetchStarted<ChatListState>());
|
||||
Object? capturedError;
|
||||
try {
|
||||
final rooms = await repo.data.getRooms(
|
||||
@@ -82,6 +107,64 @@ class ChatListBloc
|
||||
await refresh();
|
||||
}
|
||||
|
||||
/// Optimistically clears the unread counter for [token] so the tile
|
||||
/// reacts before a refresh roundtrip lands. Server-side mark-as-read
|
||||
/// is the caller's job (see [ChatBloc.sendServerReadMarker]).
|
||||
void markRoomAsRead(String token, int lastMessageId) {
|
||||
_mutateRoom(token, (r) {
|
||||
if (r.unreadMessages == 0 && r.lastReadMessage >= lastMessageId) {
|
||||
return false;
|
||||
}
|
||||
r.unreadMessages = 0;
|
||||
r.unreadMention = false;
|
||||
r.unreadMentionDirect = false;
|
||||
if (lastMessageId > r.lastReadMessage) r.lastReadMessage = lastMessageId;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/// Pushes a freshly-received message into the matching room tile so the
|
||||
/// list shows the right preview text + activity timestamp before the
|
||||
/// next full refresh lands. Also clears unread because the long-poll
|
||||
/// only feeds this in for an actively-open chat.
|
||||
void applyIncomingMessage(String token, GetChatResponseObject message) {
|
||||
_mutateRoom(token, (r) {
|
||||
final wasRead =
|
||||
r.unreadMessages == 0 && r.lastReadMessage >= message.id;
|
||||
final hasNewer = r.lastMessage.id >= message.id;
|
||||
if (wasRead && hasNewer) return false;
|
||||
r.unreadMessages = 0;
|
||||
r.unreadMention = false;
|
||||
r.unreadMentionDirect = false;
|
||||
if (message.id > r.lastReadMessage) r.lastReadMessage = message.id;
|
||||
if (message.id > r.lastMessage.id) r.lastMessage = message;
|
||||
if (message.timestamp > r.lastActivity) r.lastActivity = message.timestamp;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/// Mutates the room with [token] in-place via [mutator] (returning
|
||||
/// true if anything changed) and re-emits if so. Builds a fresh
|
||||
/// [GetRoomResponse] so equality-by-identity in the bloc state
|
||||
/// recognises the change and rebuilds.
|
||||
void _mutateRoom(
|
||||
String token,
|
||||
bool Function(GetRoomResponseObject room) mutator,
|
||||
) {
|
||||
final rooms = innerState?.rooms;
|
||||
if (rooms == null) return;
|
||||
var changed = false;
|
||||
final updated = rooms.data.map((r) {
|
||||
if (r.token != token) return r;
|
||||
if (mutator(r)) changed = true;
|
||||
return r;
|
||||
}).toSet();
|
||||
if (!changed) return;
|
||||
final newRooms = GetRoomResponse(updated)..headers = rooms.headers;
|
||||
add(Emit((s) => s.copyWith(rooms: newRooms)));
|
||||
_updateAppBadge(newRooms);
|
||||
}
|
||||
|
||||
void _updateAppBadge(GetRoomResponse rooms) {
|
||||
try {
|
||||
final unread = rooms.data.fold<int>(
|
||||
|
||||
Reference in New Issue
Block a user