feat(chat): догрузка сообщений

This commit is contained in:
torvalds
2026-06-22 22:03:56 +07:00
parent c945301350
commit ea5bf8cb9e
3 changed files with 211 additions and 16 deletions
+35
View File
@@ -0,0 +1,35 @@
import '../../backend/modules/messages.dart';
class CachedChatMessages {
final List<CachedMessage> messages;
final bool reachedStart;
const CachedChatMessages(this.messages, this.reachedStart);
}
class MessageSessionCache {
static final Map<String, CachedChatMessages> _store = {};
static String _key(int accountId, int chatId) => '$accountId:$chatId';
static CachedChatMessages? get(int accountId, int chatId) =>
_store[_key(accountId, chatId)];
static void save(
int accountId,
int chatId,
List<CachedMessage> messages, {
required bool reachedStart,
}) {
if (messages.isEmpty) return;
_store[_key(accountId, chatId)] = CachedChatMessages(
List<CachedMessage>.of(messages),
reachedStart,
);
}
static void remove(int accountId, int chatId) =>
_store.remove(_key(accountId, chatId));
static void clearAll() => _store.clear();
}
+19
View File
@@ -712,6 +712,25 @@ class AppDatabase {
); );
} }
static Future<List<Map<String, dynamic>>> loadMessagesBefore(
int accountId,
int chatId, {
required int beforeTime,
int limit = 30,
bool onlyVisible = false,
}) async {
final db = await _instance;
return db.query(
'messages',
where: onlyVisible
? 'account_id = ? AND chat_id = ? AND deleted = 0 AND time < ?'
: 'account_id = ? AND chat_id = ? AND time < ?',
whereArgs: [accountId, chatId, beforeTime],
orderBy: 'time DESC',
limit: limit,
);
}
static Future<void> markMessageDeleted( static Future<void> markMessageDeleted(
int accountId, int accountId,
int chatId, int chatId,
+157 -16
View File
@@ -30,6 +30,7 @@ import '../../../core/protocol/packet.dart';
import '../../../core/storage/app_database.dart'; import '../../../core/storage/app_database.dart';
import '../../../core/storage/draft_store.dart'; import '../../../core/storage/draft_store.dart';
import '../../../core/cache/info_cache.dart'; import '../../../core/cache/info_cache.dart';
import '../../../core/cache/message_session_cache.dart';
import '../../../core/utils/haptics.dart'; import '../../../core/utils/haptics.dart';
import '../../../core/config/app_cache_extent.dart'; import '../../../core/config/app_cache_extent.dart';
import '../../../core/config/app_message_actions_style.dart'; import '../../../core/config/app_message_actions_style.dart';
@@ -180,6 +181,13 @@ class _ChatScreenState extends State<ChatScreen>
List<CachedMessage> _messages = []; List<CachedMessage> _messages = [];
final ValueNotifier<int> _messagesRev = ValueNotifier(0); final ValueNotifier<int> _messagesRev = ValueNotifier(0);
final Set<String> _deletingIds = {}; final Set<String> _deletingIds = {};
static const int _historyPageSize = 30;
static const int _historyInitialLimit = 50;
static const double _avgMessageHeight = 72.0;
static const double _historyPrefetchExtent = _avgMessageHeight * 8;
bool _isLoadingMore = false;
bool _hasMoreHistory = true;
List<Object>? _combinedItemsCache; List<Object>? _combinedItemsCache;
int? _combinedItemsKey; int? _combinedItemsKey;
bool _floatingDateScheduled = false; bool _floatingDateScheduled = false;
@@ -207,6 +215,7 @@ class _ChatScreenState extends State<ChatScreen>
ChatsModule.chatsChanged.addListener(_onChatsBump); ChatsModule.chatsChanged.addListener(_onChatsBump);
_messageController.addListener(_onTextChanged); _messageController.addListener(_onTextChanged);
_scrollController.addListener(_onScrollForDate); _scrollController.addListener(_onScrollForDate);
_scrollController.addListener(_maybeLoadMoreHistory);
AppVisualStyle.current.addListener(_onVisualStyleChanged); AppVisualStyle.current.addListener(_onVisualStyleChanged);
_shimmerController = AnimationController( _shimmerController = AnimationController(
vsync: this, vsync: this,
@@ -291,6 +300,19 @@ class _ChatScreenState extends State<ChatScreen>
}) })
.catchError((_) {}); .catchError((_) {});
final cached = MessageSessionCache.get(_myId, widget.chatId);
if (cached != null && cached.messages.isNotEmpty) {
setState(() {
_messages = List<CachedMessage>.of(cached.messages);
_hasMoreHistory = !cached.reachedStart;
_messagesRev.value++;
_isLoading = false;
_onLoadingFinished();
});
_syncReactionNotifiersFromMessages();
return;
}
final firstRows = await AppDatabase.loadMessages( final firstRows = await AppDatabase.loadMessages(
_myId, _myId,
widget.chatId, widget.chatId,
@@ -416,11 +438,11 @@ class _ChatScreenState extends State<ChatScreen>
final fullRows = await AppDatabase.loadMessages( final fullRows = await AppDatabase.loadMessages(
_myId, _myId,
widget.chatId, widget.chatId,
limit: 100, limit: _historyInitialLimit,
onlyVisible: onlyVisible, onlyVisible: onlyVisible,
); );
final fullDecoded = await CachedMessage.fromDbRowsAsync(fullRows); final fullDecoded = await CachedMessage.fromDbRowsAsync(fullRows);
if (mounted && fullDecoded.length > _messages.length) { if (mounted) {
_applyMergedMessages(fullDecoded); _applyMergedMessages(fullDecoded);
} }
@@ -458,7 +480,7 @@ class _ChatScreenState extends State<ChatScreen>
final updatedRows = await AppDatabase.loadMessages( final updatedRows = await AppDatabase.loadMessages(
_myId, _myId,
widget.chatId, widget.chatId,
limit: 100, limit: _historyInitialLimit,
onlyVisible: onlyVisible, onlyVisible: onlyVisible,
); );
final updatedDecoded = await CachedMessage.fromDbRowsAsync(updatedRows); final updatedDecoded = await CachedMessage.fromDbRowsAsync(updatedRows);
@@ -480,19 +502,123 @@ class _ChatScreenState extends State<ChatScreen>
} }
} }
void _maybeLoadMoreHistory() {
if (!_scrollController.hasClients) return;
if (_isLoading || _isLoadingMore || !_hasMoreHistory) return;
if (_messages.isEmpty) return;
final pos = _scrollController.position;
if (pos.maxScrollExtent <= 0) return;
if (pos.maxScrollExtent - pos.pixels <= _historyPrefetchExtent) {
unawaited(_loadMoreHistory());
}
}
Future<void> _loadMoreHistory() async {
if (_isLoadingMore || !_hasMoreHistory || _messages.isEmpty) return;
_isLoadingMore = true;
setState(() {});
final oldest = _messages.first;
final onlyVisible = !KometSettings.viewDeleted.value;
try {
var older = await _loadOlderFromDb(oldest.time, onlyVisible);
if (older.length < _historyPageSize) {
final fetched = await messagesModule.fetchHistory(
_myId,
widget.chatId,
fromTime: oldest.time,
count: _historyPageSize,
);
if (fetched.isNotEmpty) {
if (KometSettings.viewDeleted.value) {
await ChatsModule.reconcileDeletedFromFetch(
_myId,
widget.chatId,
fetched,
);
}
older = await _loadOlderFromDb(oldest.time, onlyVisible);
}
}
if (!mounted) return;
final added = _prependOlder(older);
setState(() {
_isLoadingMore = false;
if (added == 0) _hasMoreHistory = false;
});
_persistSessionCache();
} catch (e) {
logger.e('Error loading more history: $e');
if (mounted) setState(() => _isLoadingMore = false);
}
}
Future<List<CachedMessage>> _loadOlderFromDb(
int beforeTime,
bool onlyVisible,
) async {
final rows = await AppDatabase.loadMessagesBefore(
_myId,
widget.chatId,
beforeTime: beforeTime,
limit: _historyPageSize,
onlyVisible: onlyVisible,
);
return CachedMessage.fromDbRowsAsync(rows);
}
int _prependOlder(List<CachedMessage> olderDesc) {
if (olderDesc.isEmpty) return 0;
final existing = _messages.map((m) => m.id).toSet();
final toAdd = <CachedMessage>[];
for (final m in olderDesc.reversed) {
if (existing.add(m.id)) toAdd.add(m);
}
if (toAdd.isEmpty) return 0;
_messages = [...toAdd, ..._messages];
_messagesRev.value++;
_syncReactionNotifiersFromMessages();
return toAdd.length;
}
void _persistSessionCache() {
if (_myId == 0 || _messages.isEmpty) return;
MessageSessionCache.save(
_myId,
widget.chatId,
_messages,
reachedStart: !_hasMoreHistory,
);
}
void _applyMergedMessages( void _applyMergedMessages(
List<CachedMessage> decodedDesc, { List<CachedMessage> decodedDesc, {
bool markLoaded = false, bool markLoaded = false,
}) { }) {
final byId = <String, CachedMessage>{for (final m in _messages) m.id: m}; final byId = <String, CachedMessage>{for (final m in _messages) m.id: m};
final merged = <CachedMessage>[]; var changed = false;
for (final fresh in decodedDesc.reversed) { for (final fresh in decodedDesc) {
final old = byId[fresh.id]; final old = byId[fresh.id];
merged.add(old != null && _sameMessage(old, fresh) ? old : fresh); if (old == null) {
byId[fresh.id] = fresh;
changed = true;
} else if (!_sameMessage(old, fresh)) {
byId[fresh.id] = fresh;
changed = true;
}
} }
final changed = !_listsEquivalent(_messages, merged);
if (!changed && !markLoaded) return; if (!changed && !markLoaded) return;
final merged = byId.values.toList()
..sort((a, b) {
final byTime = a.time.compareTo(b.time);
return byTime != 0 ? byTime : a.id.compareTo(b.id);
});
setState(() { setState(() {
if (changed) { if (changed) {
_messages = merged; _messages = merged;
@@ -506,6 +632,7 @@ class _ChatScreenState extends State<ChatScreen>
if (changed) { if (changed) {
_syncReactionNotifiersFromMessages(); _syncReactionNotifiersFromMessages();
_pruneReactionNotifiers(); _pruneReactionNotifiers();
_persistSessionCache();
} }
} }
@@ -541,14 +668,6 @@ class _ChatScreenState extends State<ChatScreen>
a.deleted == b.deleted; a.deleted == b.deleted;
} }
bool _listsEquivalent(List<CachedMessage> a, List<CachedMessage> b) {
if (a.length != b.length) return false;
for (var i = 0; i < a.length; i++) {
if (!identical(a[i], b[i])) return false;
}
return true;
}
@override @override
void deactivate() { void deactivate() {
_saveDraft(); _saveDraft();
@@ -566,6 +685,7 @@ class _ChatScreenState extends State<ChatScreen>
@override @override
void dispose() { void dispose() {
_persistSessionCache();
if (_previewChat) { if (_previewChat) {
unawaited(ChatsModule.subscribeChat(api, widget.chatId, subscribe: false)); unawaited(ChatsModule.subscribeChat(api, widget.chatId, subscribe: false));
} }
@@ -575,6 +695,7 @@ class _ChatScreenState extends State<ChatScreen>
_saveDraft(); _saveDraft();
_messageController.removeListener(_onTextChanged); _messageController.removeListener(_onTextChanged);
_scrollController.removeListener(_onScrollForDate); _scrollController.removeListener(_onScrollForDate);
_scrollController.removeListener(_maybeLoadMoreHistory);
AppVisualStyle.current.removeListener(_onVisualStyleChanged); AppVisualStyle.current.removeListener(_onVisualStyleChanged);
_floatingDateTimer?.cancel(); _floatingDateTimer?.cancel();
_floatingDateCurved.dispose(); _floatingDateCurved.dispose();
@@ -2873,6 +2994,23 @@ class _ChatScreenState extends State<ChatScreen>
); );
} }
Widget _buildLoadMoreIndicator() {
final cs = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Center(
child: SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2.2,
color: cs.onSurfaceVariant,
),
),
),
);
}
Widget _buildMessagesListContent() { Widget _buildMessagesListContent() {
if (_messages.isEmpty) { if (_messages.isEmpty) {
return Center( return Center(
@@ -2899,8 +3037,11 @@ class _ChatScreenState extends State<ChatScreen>
reverse: true, reverse: true,
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
cacheExtent: cacheExtent, cacheExtent: cacheExtent,
itemCount: items.length, itemCount: items.length + (_isLoadingMore ? 1 : 0),
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index >= items.length) {
return _buildLoadMoreIndicator();
}
final item = items[items.length - 1 - index]; final item = items[items.length - 1 - index];
if (item is _DateSeparatorItem) { if (item is _DateSeparatorItem) {