Merge branch 'feature/FullStack' of https://github.com/KometTeam/Komet into feature/FullStack
This commit is contained in:
+35
@@ -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();
|
||||
}
|
||||
@@ -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(
|
||||
int accountId,
|
||||
int chatId,
|
||||
|
||||
@@ -31,6 +31,7 @@ import '../../../core/protocol/packet.dart';
|
||||
import '../../../core/storage/app_database.dart';
|
||||
import '../../../core/storage/draft_store.dart';
|
||||
import '../../../core/cache/info_cache.dart';
|
||||
import '../../../core/cache/message_session_cache.dart';
|
||||
import '../../../core/utils/haptics.dart';
|
||||
import '../../../core/config/app_cache_extent.dart';
|
||||
import '../../../core/config/app_message_actions_style.dart';
|
||||
@@ -181,6 +182,13 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
List<CachedMessage> _messages = [];
|
||||
final ValueNotifier<int> _messagesRev = ValueNotifier(0);
|
||||
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;
|
||||
int? _combinedItemsKey;
|
||||
bool _floatingDateScheduled = false;
|
||||
@@ -208,6 +216,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
ChatsModule.chatsChanged.addListener(_onChatsBump);
|
||||
_messageController.addListener(_onTextChanged);
|
||||
_scrollController.addListener(_onScrollForDate);
|
||||
_scrollController.addListener(_maybeLoadMoreHistory);
|
||||
AppVisualStyle.current.addListener(_onVisualStyleChanged);
|
||||
_shimmerController = AnimationController(
|
||||
vsync: this,
|
||||
@@ -292,6 +301,19 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
})
|
||||
.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(
|
||||
_myId,
|
||||
widget.chatId,
|
||||
@@ -417,11 +439,11 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
final fullRows = await AppDatabase.loadMessages(
|
||||
_myId,
|
||||
widget.chatId,
|
||||
limit: 100,
|
||||
limit: _historyInitialLimit,
|
||||
onlyVisible: onlyVisible,
|
||||
);
|
||||
final fullDecoded = await CachedMessage.fromDbRowsAsync(fullRows);
|
||||
if (mounted && fullDecoded.length > _messages.length) {
|
||||
if (mounted) {
|
||||
_applyMergedMessages(fullDecoded);
|
||||
}
|
||||
|
||||
@@ -459,7 +481,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
final updatedRows = await AppDatabase.loadMessages(
|
||||
_myId,
|
||||
widget.chatId,
|
||||
limit: 100,
|
||||
limit: _historyInitialLimit,
|
||||
onlyVisible: onlyVisible,
|
||||
);
|
||||
final updatedDecoded = await CachedMessage.fromDbRowsAsync(updatedRows);
|
||||
@@ -481,19 +503,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(
|
||||
List<CachedMessage> decodedDesc, {
|
||||
bool markLoaded = false,
|
||||
}) {
|
||||
final byId = <String, CachedMessage>{for (final m in _messages) m.id: m};
|
||||
final merged = <CachedMessage>[];
|
||||
for (final fresh in decodedDesc.reversed) {
|
||||
var changed = false;
|
||||
for (final fresh in decodedDesc) {
|
||||
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;
|
||||
|
||||
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(() {
|
||||
if (changed) {
|
||||
_messages = merged;
|
||||
@@ -507,6 +633,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
if (changed) {
|
||||
_syncReactionNotifiersFromMessages();
|
||||
_pruneReactionNotifiers();
|
||||
_persistSessionCache();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -542,14 +669,6 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
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
|
||||
void deactivate() {
|
||||
_saveDraft();
|
||||
@@ -567,6 +686,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_persistSessionCache();
|
||||
if (_previewChat) {
|
||||
unawaited(ChatsModule.subscribeChat(api, widget.chatId, subscribe: false));
|
||||
}
|
||||
@@ -576,6 +696,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
_saveDraft();
|
||||
_messageController.removeListener(_onTextChanged);
|
||||
_scrollController.removeListener(_onScrollForDate);
|
||||
_scrollController.removeListener(_maybeLoadMoreHistory);
|
||||
AppVisualStyle.current.removeListener(_onVisualStyleChanged);
|
||||
_floatingDateTimer?.cancel();
|
||||
_floatingDateCurved.dispose();
|
||||
@@ -991,7 +1112,11 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
}
|
||||
|
||||
void _replySelected() {
|
||||
showCustomNotification(context, 'Ответ — пока в разработке');
|
||||
final msgs = _selectedMessages(_selectedIds.value);
|
||||
if (msgs.isEmpty) return;
|
||||
final message = msgs.first;
|
||||
_clearSelection();
|
||||
_startReply(message);
|
||||
}
|
||||
|
||||
void _forwardSelected() {
|
||||
@@ -2916,6 +3041,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() {
|
||||
if (_messages.isEmpty) {
|
||||
return Center(
|
||||
@@ -2942,8 +3084,11 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
reverse: true,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
cacheExtent: cacheExtent,
|
||||
itemCount: items.length,
|
||||
itemCount: items.length + (_isLoadingMore ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index >= items.length) {
|
||||
return _buildLoadMoreIndicator();
|
||||
}
|
||||
final item = items[items.length - 1 - index];
|
||||
|
||||
if (item is _DateSeparatorItem) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:io';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:komet/main.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import '../../backend/modules/messages.dart';
|
||||
@@ -54,6 +55,22 @@ class _BubbleCtx {
|
||||
final Expando<MessageType> _contentTypeCache = Expando<MessageType>();
|
||||
final Expando<({bool full, String text})> _clockTextCache = Expando();
|
||||
|
||||
class _ZeroIntrinsicWidth extends SingleChildRenderObjectWidget {
|
||||
const _ZeroIntrinsicWidth({required Widget super.child});
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) =>
|
||||
_RenderZeroIntrinsicWidth();
|
||||
}
|
||||
|
||||
class _RenderZeroIntrinsicWidth extends RenderProxyBox {
|
||||
@override
|
||||
double computeMinIntrinsicWidth(double height) => 0;
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicWidth(double height) => 0;
|
||||
}
|
||||
|
||||
class MessageBubble extends StatelessWidget {
|
||||
static const double photoMaxSize = 280.0;
|
||||
static const double photoMinSize = 100.0;
|
||||
@@ -375,14 +392,24 @@ class MessageBubble extends StatelessWidget {
|
||||
final reply = message.replyInfo;
|
||||
Widget withReply(Widget content) {
|
||||
if (reply == null) return content;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildReplyQuote(context, cs, textColor, reply),
|
||||
const SizedBox(height: 4),
|
||||
content,
|
||||
],
|
||||
final quote = _buildReplyQuote(context, cs, textColor, reply);
|
||||
if (contentType != MessageType.text) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [quote, const SizedBox(height: 4), content],
|
||||
);
|
||||
}
|
||||
return IntrinsicWidth(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_ZeroIntrinsicWidth(child: quote),
|
||||
const SizedBox(height: 4),
|
||||
content,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user