implemented backward pagination for chat history with on-scroll prefetching
This commit is contained in:
@@ -4,6 +4,7 @@ import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../../../../../api/marianumcloud/talk/chat/get_chat_history.dart';
|
||||
import '../../../../../api/marianumcloud/talk/chat/get_chat_response.dart';
|
||||
import '../../../../../api/marianumcloud/talk/chat/long_poll_chat.dart';
|
||||
import '../../../../../api/marianumcloud/talk/room/get_room_response.dart';
|
||||
@@ -56,7 +57,22 @@ class ChatBloc
|
||||
ChatState fromStorage(Map<String, dynamic> json) => ChatState.fromJson(json);
|
||||
|
||||
@override
|
||||
Map<String, dynamic>? toStorage(ChatState state) => state.toJson();
|
||||
Map<String, dynamic>? toStorage(ChatState state) {
|
||||
final response = state.chatResponse;
|
||||
if (response == null ||
|
||||
response.data.length <= _kMaxPersistedMessages) {
|
||||
return state.toJson();
|
||||
}
|
||||
// Keep only the newest N; trimming drops older messages, so there is
|
||||
// definitely more history to page back in after a restart.
|
||||
final newest = response
|
||||
.sortByTimestamp()
|
||||
.reversed
|
||||
.take(_kMaxPersistedMessages)
|
||||
.toSet();
|
||||
final trimmed = GetChatResponse(newest)..headers = response.headers;
|
||||
return state.copyWith(chatResponse: trimmed, hasMoreOld: true).toJson();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> gatherData() async {
|
||||
@@ -75,7 +91,16 @@ class ChatBloc
|
||||
return;
|
||||
}
|
||||
_stopLongPoll();
|
||||
add(Emit((s) => s.copyWith(currentToken: token, chatResponse: null)));
|
||||
add(
|
||||
Emit(
|
||||
(s) => s.copyWith(
|
||||
currentToken: token,
|
||||
chatResponse: null,
|
||||
hasMoreOld: true,
|
||||
isLoadingOlder: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
add(RefetchStarted<ChatState>());
|
||||
_scheduleLoad(token);
|
||||
}
|
||||
@@ -100,6 +125,40 @@ class ChatBloc
|
||||
_stopLongPoll();
|
||||
}
|
||||
|
||||
/// Pages in the next block of messages older than the oldest currently held.
|
||||
/// No-ops when nothing more can be loaded or a load is already in flight.
|
||||
Future<void> loadOlder() async {
|
||||
final state = innerState;
|
||||
if (state == null) return;
|
||||
final token = state.currentToken;
|
||||
if (token.isEmpty) return;
|
||||
if (state.isLoadingOlder || !state.hasMoreOld) return;
|
||||
final response = state.chatResponse;
|
||||
if (response == null) return;
|
||||
final oldestId = _minMessageId(response);
|
||||
if (oldestId <= 0) return;
|
||||
|
||||
add(Emit((s) => s.copyWith(isLoadingOlder: true)));
|
||||
try {
|
||||
final older = await GetChatHistory(
|
||||
chatToken: token,
|
||||
lastKnownMessageId: oldestId,
|
||||
limit: _kOlderPageSize,
|
||||
).run();
|
||||
if (isClosed) return;
|
||||
if ((innerState?.currentToken ?? '') != token) return;
|
||||
if (older == null || older.data.isEmpty) {
|
||||
add(Emit((s) => s.copyWith(hasMoreOld: false, isLoadingOlder: false)));
|
||||
return;
|
||||
}
|
||||
_applyOlderResponse(older);
|
||||
} on Object catch (e) {
|
||||
log('Load older messages for $token failed: $e');
|
||||
if (isClosed) return;
|
||||
add(Emit((s) => s.copyWith(isLoadingOlder: false)));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendServerReadMarker(String token, int messageId) async {
|
||||
try {
|
||||
await SetReadMarker(
|
||||
@@ -245,7 +304,15 @@ class ChatBloc
|
||||
void _applyChatResponse(GetChatResponse incoming) {
|
||||
final current = innerState?.chatResponse;
|
||||
if (current == null) {
|
||||
add(DataGathered((s) => s.copyWith(chatResponse: incoming)));
|
||||
// Initial load: a short first page means there is nothing older to page in.
|
||||
add(
|
||||
DataGathered(
|
||||
(s) => s.copyWith(
|
||||
chatResponse: incoming,
|
||||
hasMoreOld: incoming.data.length >= _kInitialPageSize,
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final byId = <int, GetChatResponseObject>{};
|
||||
@@ -260,6 +327,32 @@ class ChatBloc
|
||||
add(DataGathered((s) => s.copyWith(chatResponse: merged)));
|
||||
}
|
||||
|
||||
/// Merges an older history page. Unlike [_applyChatResponse] it keeps the
|
||||
/// current headers — the older page's `x-chat-last-common-read` would regress
|
||||
/// the read-status shown for already-visible messages.
|
||||
void _applyOlderResponse(GetChatResponse older) {
|
||||
final current = innerState?.chatResponse;
|
||||
if (current == null) return;
|
||||
final byId = <int, GetChatResponseObject>{};
|
||||
for (final m in current.data) {
|
||||
byId[m.id] = m;
|
||||
}
|
||||
for (final m in older.data) {
|
||||
byId.putIfAbsent(m.id, () => m);
|
||||
}
|
||||
final merged = GetChatResponse(byId.values.toSet())
|
||||
..headers = current.headers;
|
||||
add(
|
||||
DataGathered(
|
||||
(s) => s.copyWith(
|
||||
chatResponse: merged,
|
||||
hasMoreOld: older.data.length >= _kOlderPageSize,
|
||||
isLoadingOlder: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
int _maxMessageId(GetChatResponse? response) {
|
||||
if (response == null) return 0;
|
||||
var max = 0;
|
||||
@@ -269,6 +362,16 @@ class ChatBloc
|
||||
return max;
|
||||
}
|
||||
|
||||
int _minMessageId(GetChatResponse? response) {
|
||||
if (response == null) return 0;
|
||||
var min = 0;
|
||||
for (final m in response.data) {
|
||||
if (m.id <= 0) continue; // skip dummies
|
||||
if (min == 0 || m.id < min) min = m.id;
|
||||
}
|
||||
return min;
|
||||
}
|
||||
|
||||
/// Mirrors the server's own `lastMessage` selection (comments + voice only).
|
||||
GetChatResponseObject? _pickDisplayMessage(GetChatResponse response) {
|
||||
GetChatResponseObject? best;
|
||||
@@ -288,3 +391,16 @@ class ChatBloc
|
||||
}
|
||||
|
||||
const _kLongPollLastGivenHeader = 'x-chat-last-given';
|
||||
|
||||
/// Small first page shown on open (keeps the initial request cheap). Must match
|
||||
/// the `limit` in GetChatCache (get_chat_cache.dart).
|
||||
const _kInitialPageSize = 50;
|
||||
|
||||
/// Upper bound on how many messages are persisted via HydratedBloc. The full
|
||||
/// scrolled history stays in memory at runtime; only the newest this-many
|
||||
/// survive a restart (older ones are re-fetchable via scroll-up).
|
||||
const _kMaxPersistedMessages = 500;
|
||||
|
||||
/// Larger block fetched per scroll-up so paging back through history needs
|
||||
/// fewer round trips.
|
||||
const _kOlderPageSize = 200;
|
||||
|
||||
Reference in New Issue
Block a user