оптимизация: слегоньца набурмалдил
This commit is contained in:
@@ -512,12 +512,15 @@ class ChatsModule {
|
|||||||
if (accountId == null) return;
|
if (accountId == null) return;
|
||||||
|
|
||||||
final dialogRows = await AppDatabase.loadDialogChats(accountId);
|
final dialogRows = await AppDatabase.loadDialogChats(accountId);
|
||||||
final byParticipant = <int, List<Map<String, dynamic>>>{};
|
final byParticipant =
|
||||||
|
<int, List<({Map<String, dynamic> row, CachedChat cached})>>{};
|
||||||
for (final row in dialogRows) {
|
for (final row in dialogRows) {
|
||||||
final cached = CachedChat.fromDbRow(row);
|
final cached = CachedChat.fromDbRow(row);
|
||||||
for (final pid in cached.participants.keys) {
|
for (final pid in cached.participants.keys) {
|
||||||
if (pid == accountId) continue;
|
if (pid == accountId) continue;
|
||||||
byParticipant.putIfAbsent(pid, () => []).add(row);
|
byParticipant
|
||||||
|
.putIfAbsent(pid, () => [])
|
||||||
|
.add((row: row, cached: cached));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,8 +532,9 @@ class ChatsModule {
|
|||||||
final options = ContactCache.getOptions(contactId) ?? const <String>{};
|
final options = ContactCache.getOptions(contactId) ?? const <String>{};
|
||||||
final affected = byParticipant[contactId];
|
final affected = byParticipant[contactId];
|
||||||
if (affected == null) continue;
|
if (affected == null) continue;
|
||||||
for (final row in affected) {
|
for (final entry in affected) {
|
||||||
final cached = CachedChat.fromDbRow(row);
|
final row = entry.row;
|
||||||
|
final cached = entry.cached;
|
||||||
final sameTitle = cached.title == name;
|
final sameTitle = cached.title == name;
|
||||||
final sameAvatar = (cached.iconUrl ?? '') == (avatar ?? '');
|
final sameAvatar = (cached.iconUrl ?? '') == (avatar ?? '');
|
||||||
final sameOptions = cached.options.length == options.length &&
|
final sameOptions = cached.options.length == options.length &&
|
||||||
|
|||||||
@@ -44,10 +44,13 @@ class FoldersModule {
|
|||||||
List<dynamic>? foldersOrder,
|
List<dynamic>? foldersOrder,
|
||||||
) {
|
) {
|
||||||
if (foldersOrder == null || foldersOrder.isEmpty) return;
|
if (foldersOrder == null || foldersOrder.isEmpty) return;
|
||||||
final orderedIds = foldersOrder.map((id) => id.toString()).toList();
|
final orderIndex = <String, int>{};
|
||||||
|
for (var i = 0; i < foldersOrder.length; i++) {
|
||||||
|
orderIndex.putIfAbsent(foldersOrder[i].toString(), () => i);
|
||||||
|
}
|
||||||
folders.sort((a, b) {
|
folders.sort((a, b) {
|
||||||
final aIndex = orderedIds.indexOf(a.id);
|
final aIndex = orderIndex[a.id] ?? -1;
|
||||||
final bIndex = orderedIds.indexOf(b.id);
|
final bIndex = orderIndex[b.id] ?? -1;
|
||||||
if (aIndex == -1 && bIndex == -1) return 0;
|
if (aIndex == -1 && bIndex == -1) return 0;
|
||||||
if (aIndex == -1) return 1;
|
if (aIndex == -1) return 1;
|
||||||
if (bIndex == -1) return -1;
|
if (bIndex == -1) return -1;
|
||||||
|
|||||||
@@ -92,25 +92,31 @@ const int _compressionThreshold = 32;
|
|||||||
/// `0` — без сжатия, иначе `(rawLen ~/ compLen) + 1` (множитель размера, по
|
/// `0` — без сжатия, иначе `(rawLen ~/ compLen) + 1` (множитель размера, по
|
||||||
/// которому получатель выделяет буфер под распаковку).
|
/// которому получатель выделяет буфер под распаковку).
|
||||||
Uint8List packPacket(int opcode, Map<dynamic, dynamic> payload, {int seq = 0}) {
|
Uint8List packPacket(int opcode, Map<dynamic, dynamic> payload, {int seq = 0}) {
|
||||||
final header = ByteData(headerSize);
|
final Uint8List raw = msgpack.serialize(payload);
|
||||||
|
|
||||||
|
final List<int> body;
|
||||||
|
final int flag;
|
||||||
|
if (raw.length < _compressionThreshold) {
|
||||||
|
body = raw;
|
||||||
|
flag = 0;
|
||||||
|
} else {
|
||||||
|
body = lz4Compress(raw);
|
||||||
|
flag = (raw.length ~/ body.length) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
final out = Uint8List(headerSize + body.length);
|
||||||
|
final header = ByteData.view(out.buffer, out.offsetInBytes, headerSize);
|
||||||
header.setUint8(0, 10);
|
header.setUint8(0, 10);
|
||||||
header.setUint8(1, CmdType.request);
|
header.setUint8(1, CmdType.request);
|
||||||
header.setUint16(2, seq, Endian.big);
|
header.setUint16(2, seq, Endian.big);
|
||||||
header.setUint16(4, opcode, Endian.big);
|
header.setUint16(4, opcode, Endian.big);
|
||||||
|
header.setUint32(
|
||||||
final raw = Uint8List.fromList(msgpack.serialize(payload));
|
6,
|
||||||
|
((flag & 0xFF) << 24) | (body.length & 0xFFFFFF),
|
||||||
if (raw.length < _compressionThreshold) {
|
Endian.big,
|
||||||
header.setUint32(6, raw.length & 0xFFFFFF, Endian.big);
|
);
|
||||||
return Uint8List.fromList(header.buffer.asUint8List() + raw);
|
out.setRange(headerSize, out.length, body);
|
||||||
}
|
return out;
|
||||||
|
|
||||||
final compressed = lz4Compress(raw);
|
|
||||||
final compLen = compressed.length;
|
|
||||||
final flag = (raw.length ~/ compLen) + 1;
|
|
||||||
header.setUint32(6, ((flag & 0xFF) << 24) | (compLen & 0xFFFFFF), Endian.big);
|
|
||||||
|
|
||||||
return Uint8List.fromList(header.buffer.asUint8List() + compressed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Распаковка пакета от сервера
|
/// Распаковка пакета от сервера
|
||||||
|
|||||||
@@ -435,17 +435,20 @@ class AppDatabase {
|
|||||||
// Chats cache
|
// Chats cache
|
||||||
|
|
||||||
static Future<void> saveChats(List<Map<String, dynamic>> rows) async {
|
static Future<void> saveChats(List<Map<String, dynamic>> rows) async {
|
||||||
|
if (rows.isEmpty) return;
|
||||||
try {
|
try {
|
||||||
final db = await _instance;
|
final db = await _instance;
|
||||||
final batch = db.batch();
|
await db.transaction((txn) async {
|
||||||
for (final row in rows) {
|
final batch = txn.batch();
|
||||||
batch.insert(
|
for (final row in rows) {
|
||||||
'chats_cache',
|
batch.insert(
|
||||||
row,
|
'chats_cache',
|
||||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
row,
|
||||||
);
|
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||||
}
|
);
|
||||||
await batch.commit(noResult: true);
|
}
|
||||||
|
await batch.commit(noResult: true);
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.e("Ошибка при сохранении чата: $e");
|
logger.e("Ошибка при сохранении чата: $e");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,46 +7,73 @@ import '../utils/logger.dart';
|
|||||||
/// Копит сырые байты из сокета, нарезает их на байтовые срезы целых пакетов.
|
/// Копит сырые байты из сокета, нарезает их на байтовые срезы целых пакетов.
|
||||||
class PacketReceiver {
|
class PacketReceiver {
|
||||||
Uint8List _buffer = Uint8List(0);
|
Uint8List _buffer = Uint8List(0);
|
||||||
|
int _start = 0;
|
||||||
|
int _end = 0;
|
||||||
|
|
||||||
static const int _maxBufferSize = 2 * 1024 * 1024; // 2 мегабуйта
|
static const int _maxBufferSize = 2 * 1024 * 1024; // 2 мегабуйта
|
||||||
|
|
||||||
/// Добавляет байты в буфер и возвращает все собранные пакеты как сырые срезы.
|
/// Добавляет байты в буфер и возвращает все собранные пакеты как сырые срезы.
|
||||||
/// Полностью синхронный — нарезка не блокируется на распаковке, поэтому
|
/// Полностью синхронный — нарезка не блокируется на распаковке, поэтому
|
||||||
/// конкурентные вызовы из stream-листенера не могут пересечься на `_buffer`.
|
/// конкурентные вызовы из stream-листенера не могут пересечься на `_buffer`.
|
||||||
|
///
|
||||||
|
/// Накопление идёт без перекопирования всего буфера на каждый чанк: целые
|
||||||
|
/// пакеты отдаются как `sublistView`, а потреблённый префикс отбрасывается
|
||||||
|
/// сдвигом указателя `_start`, а не пересборкой буфера.
|
||||||
List<Uint8List> feed(Uint8List data) {
|
List<Uint8List> feed(Uint8List data) {
|
||||||
final newBuffer = Uint8List(_buffer.length + data.length);
|
_append(data);
|
||||||
newBuffer.setAll(0, _buffer);
|
|
||||||
newBuffer.setAll(_buffer.length, data);
|
|
||||||
_buffer = newBuffer;
|
|
||||||
|
|
||||||
if (_buffer.length > _maxBufferSize) {
|
if (_end - _start > _maxBufferSize) {
|
||||||
logger.e(
|
logger.e(
|
||||||
'PacketReceiver: переполнение буфера (${_buffer.length} B), сброс',
|
'PacketReceiver: переполнение буфера (${_end - _start} B), сброс',
|
||||||
);
|
);
|
||||||
reset();
|
reset();
|
||||||
return const [];
|
return const [];
|
||||||
}
|
}
|
||||||
|
|
||||||
final packets = <Uint8List>[];
|
final packets = <Uint8List>[];
|
||||||
while (_buffer.length >= headerSize) {
|
while (_end - _start >= headerSize) {
|
||||||
final bd = ByteData.view(
|
final bd = ByteData.view(
|
||||||
_buffer.buffer,
|
_buffer.buffer,
|
||||||
_buffer.offsetInBytes,
|
_buffer.offsetInBytes + _start,
|
||||||
headerSize,
|
headerSize,
|
||||||
);
|
);
|
||||||
final packedLen = bd.getUint32(6, Endian.big);
|
final packedLen = bd.getUint32(6, Endian.big);
|
||||||
final payloadLength = packedLen & 0xFFFFFF;
|
final payloadLength = packedLen & 0xFFFFFF;
|
||||||
final totalLength = headerSize + payloadLength;
|
final totalLength = headerSize + payloadLength;
|
||||||
|
|
||||||
if (_buffer.length < totalLength) break;
|
if (_end - _start < totalLength) break;
|
||||||
|
|
||||||
packets.add(Uint8List.sublistView(_buffer, 0, totalLength));
|
packets.add(Uint8List.sublistView(_buffer, _start, _start + totalLength));
|
||||||
_buffer = _buffer.sublist(totalLength);
|
_start += totalLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_start == _end) {
|
||||||
|
_start = 0;
|
||||||
|
_end = 0;
|
||||||
}
|
}
|
||||||
return packets;
|
return packets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _append(Uint8List data) {
|
||||||
|
final pending = _end - _start;
|
||||||
|
if (pending == 0) {
|
||||||
|
_buffer = Uint8List.fromList(data);
|
||||||
|
_start = 0;
|
||||||
|
_end = data.length;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final total = pending + data.length;
|
||||||
|
final newBuffer = Uint8List(total);
|
||||||
|
newBuffer.setRange(0, pending, _buffer, _start);
|
||||||
|
newBuffer.setRange(pending, total, data);
|
||||||
|
_buffer = newBuffer;
|
||||||
|
_start = 0;
|
||||||
|
_end = total;
|
||||||
|
}
|
||||||
|
|
||||||
void reset() {
|
void reset() {
|
||||||
_buffer = Uint8List(0);
|
_buffer = Uint8List(0);
|
||||||
|
_start = 0;
|
||||||
|
_end = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class MediaCache {
|
|||||||
static int get maxBytes => AppMediaCacheLimit.current.value;
|
static int get maxBytes => AppMediaCacheLimit.current.value;
|
||||||
|
|
||||||
static Directory? _dir;
|
static Directory? _dir;
|
||||||
|
static int? _cachedSize;
|
||||||
|
|
||||||
static Future<Directory> _cacheDir() async {
|
static Future<Directory> _cacheDir() async {
|
||||||
final cached = _dir;
|
final cached = _dir;
|
||||||
@@ -81,6 +82,12 @@ class MediaCache {
|
|||||||
}
|
}
|
||||||
await sink.close();
|
await sink.close();
|
||||||
await part.rename(file.path);
|
await part.rename(file.path);
|
||||||
|
final known = _cachedSize;
|
||||||
|
if (known != null) {
|
||||||
|
try {
|
||||||
|
_cachedSize = known + await file.length();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
await _enforceLimit();
|
await _enforceLimit();
|
||||||
return file;
|
return file;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@@ -96,7 +103,18 @@ class MediaCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Суммарный размер кэша в байтах.
|
/// Суммарный размер кэша в байтах.
|
||||||
|
///
|
||||||
|
/// Результат держится в памяти и поддерживается инкрементально при
|
||||||
|
/// загрузке/очистке/вытеснении — повторные вызовы не пересканируют каталог.
|
||||||
static Future<int> currentSize() async {
|
static Future<int> currentSize() async {
|
||||||
|
final cached = _cachedSize;
|
||||||
|
if (cached != null) return cached;
|
||||||
|
final total = await _scanSize();
|
||||||
|
_cachedSize = total;
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<int> _scanSize() async {
|
||||||
final dir = await _cacheDir();
|
final dir = await _cacheDir();
|
||||||
var total = 0;
|
var total = 0;
|
||||||
await for (final entity in dir.list()) {
|
await for (final entity in dir.list()) {
|
||||||
@@ -121,34 +139,43 @@ class MediaCache {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_cachedSize = 0;
|
||||||
return freed;
|
return freed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Вытесняет старые файлы (по mtime), пока размер превышает [maxBytes].
|
/// Вытесняет старые файлы (по mtime), пока размер превышает [maxBytes].
|
||||||
|
///
|
||||||
|
/// Под лимитом — ранний выход без сканирования каталога (частый случай).
|
||||||
|
/// Каталог обходится только когда лимит реально превышен.
|
||||||
static Future<void> _enforceLimit() async {
|
static Future<void> _enforceLimit() async {
|
||||||
|
final limit = maxBytes;
|
||||||
|
if (limit <= 0) return;
|
||||||
|
|
||||||
|
var total = _cachedSize ?? await _scanSize();
|
||||||
|
if (total <= limit) {
|
||||||
|
_cachedSize = total;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final dir = await _cacheDir();
|
final dir = await _cacheDir();
|
||||||
final files = <File>[];
|
final files = <File>[];
|
||||||
var total = 0;
|
|
||||||
await for (final entity in dir.list()) {
|
await for (final entity in dir.list()) {
|
||||||
if (entity is File && !entity.path.endsWith('.part')) {
|
if (entity is File && !entity.path.endsWith('.part')) {
|
||||||
files.add(entity);
|
files.add(entity);
|
||||||
try {
|
|
||||||
total += await entity.length();
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (maxBytes <= 0 || total <= maxBytes) return;
|
|
||||||
|
|
||||||
files.sort((a, b) =>
|
files.sort((a, b) =>
|
||||||
a.statSync().modified.compareTo(b.statSync().modified));
|
a.statSync().modified.compareTo(b.statSync().modified));
|
||||||
|
|
||||||
for (final file in files) {
|
for (final file in files) {
|
||||||
if (total <= maxBytes) break;
|
if (total <= limit) break;
|
||||||
try {
|
try {
|
||||||
total -= await file.length();
|
total -= await file.length();
|
||||||
await file.delete();
|
await file.delete();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
_cachedSize = total;
|
||||||
}
|
}
|
||||||
|
|
||||||
static String _sanitize(String name) {
|
static String _sanitize(String name) {
|
||||||
|
|||||||
@@ -183,8 +183,12 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
|
|
||||||
List<CachedChat> _selectedChatObjects() {
|
List<CachedChat> _selectedChatObjects() {
|
||||||
if (_selectedChats.isEmpty) return const [];
|
if (_selectedChats.isEmpty) return const [];
|
||||||
final ids = _selectedChats;
|
final ids = <int>{};
|
||||||
return _chats.where((c) => ids.contains(c.id.toString())).toList();
|
for (final s in _selectedChats) {
|
||||||
|
final v = int.tryParse(s);
|
||||||
|
if (v != null) ids.add(v);
|
||||||
|
}
|
||||||
|
return _chats.where((c) => ids.contains(c.id)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
_DeleteKind _categorizeChat(CachedChat c, int myId) {
|
_DeleteKind _categorizeChat(CachedChat c, int myId) {
|
||||||
|
|||||||
@@ -136,8 +136,10 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
Timer? _shimmerStartTimer;
|
Timer? _shimmerStartTimer;
|
||||||
bool _historyKickedOff = false;
|
bool _historyKickedOff = false;
|
||||||
List<CachedMessage> _messages = [];
|
List<CachedMessage> _messages = [];
|
||||||
|
int _messagesRevision = 0;
|
||||||
List<Object>? _combinedItemsCache;
|
List<Object>? _combinedItemsCache;
|
||||||
int? _combinedItemsKey;
|
int? _combinedItemsKey;
|
||||||
|
bool _floatingDateScheduled = false;
|
||||||
int _myId = 0;
|
int _myId = 0;
|
||||||
CachedChat? chat;
|
CachedChat? chat;
|
||||||
|
|
||||||
@@ -146,7 +148,6 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
late final AnimationController _floatingDateAnimController;
|
late final AnimationController _floatingDateAnimController;
|
||||||
late final CurvedAnimation _floatingDateCurved;
|
late final CurvedAnimation _floatingDateCurved;
|
||||||
final Map<int, GlobalKey> _separatorKeys = {};
|
final Map<int, GlobalKey> _separatorKeys = {};
|
||||||
double _lastScrollOffset = 0;
|
|
||||||
String? _lastSentId;
|
String? _lastSentId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -226,6 +227,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
.toList();
|
.toList();
|
||||||
setState(() {
|
setState(() {
|
||||||
_messages = first;
|
_messages = first;
|
||||||
|
_messagesRevision++;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
_onLoadingFinished();
|
_onLoadingFinished();
|
||||||
});
|
});
|
||||||
@@ -349,7 +351,10 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
final changed = !_listsEquivalent(_messages, merged);
|
final changed = !_listsEquivalent(_messages, merged);
|
||||||
if (!changed && !markLoaded) return;
|
if (!changed && !markLoaded) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
if (changed) _messages = merged;
|
if (changed) {
|
||||||
|
_messages = merged;
|
||||||
|
_messagesRevision++;
|
||||||
|
}
|
||||||
if (markLoaded) {
|
if (markLoaded) {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
_onLoadingFinished();
|
_onLoadingFinished();
|
||||||
@@ -590,6 +595,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
setState(() {
|
setState(() {
|
||||||
_lastSentId = message.id;
|
_lastSentId = message.id;
|
||||||
_messages.add(message);
|
_messages.add(message);
|
||||||
|
_messagesRevision++;
|
||||||
});
|
});
|
||||||
_clearTyping(message.senderId);
|
_clearTyping(message.senderId);
|
||||||
Haptics.tap();
|
Haptics.tap();
|
||||||
@@ -598,11 +604,17 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
case MessageEditedEvent(:final message):
|
case MessageEditedEvent(:final message):
|
||||||
final idx = _messages.indexWhere((m) => m.id == message.id);
|
final idx = _messages.indexWhere((m) => m.id == message.id);
|
||||||
if (idx == -1) return;
|
if (idx == -1) return;
|
||||||
setState(() => _messages[idx] = message);
|
setState(() {
|
||||||
|
_messages[idx] = message;
|
||||||
|
_messagesRevision++;
|
||||||
|
});
|
||||||
case MessageRemovedEvent(:final messageId):
|
case MessageRemovedEvent(:final messageId):
|
||||||
final idx = _messages.indexWhere((m) => m.id == messageId);
|
final idx = _messages.indexWhere((m) => m.id == messageId);
|
||||||
if (idx == -1) return;
|
if (idx == -1) return;
|
||||||
setState(() => _messages.removeAt(idx));
|
setState(() {
|
||||||
|
_messages.removeAt(idx);
|
||||||
|
_messagesRevision++;
|
||||||
|
});
|
||||||
_reactionNotifiers.remove(messageId)?.dispose();
|
_reactionNotifiers.remove(messageId)?.dispose();
|
||||||
case MessageReactionsChangedEvent(:final messageId, :final reactionInfo):
|
case MessageReactionsChangedEvent(:final messageId, :final reactionInfo):
|
||||||
_reactionNotifiers[messageId]?.value = reactionInfo;
|
_reactionNotifiers[messageId]?.value = reactionInfo;
|
||||||
@@ -722,6 +734,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
setState(() {
|
setState(() {
|
||||||
_lastSentId = tempId;
|
_lastSentId = tempId;
|
||||||
_messages.add(tempMessage);
|
_messages.add(tempMessage);
|
||||||
|
_messagesRevision++;
|
||||||
_messageController.clear();
|
_messageController.clear();
|
||||||
});
|
});
|
||||||
unawaited(_persistOutgoing(tempMessage));
|
unawaited(_persistOutgoing(tempMessage));
|
||||||
@@ -748,6 +761,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
);
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
_messages[index] = sent;
|
_messages[index] = sent;
|
||||||
|
_messagesRevision++;
|
||||||
});
|
});
|
||||||
unawaited(_persistOutgoing(sent, removeId: tempId));
|
unawaited(_persistOutgoing(sent, removeId: tempId));
|
||||||
}
|
}
|
||||||
@@ -776,6 +790,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
);
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
_messages[index] = failed;
|
_messages[index] = failed;
|
||||||
|
_messagesRevision++;
|
||||||
});
|
});
|
||||||
unawaited(_persistOutgoing(failed));
|
unawaited(_persistOutgoing(failed));
|
||||||
}
|
}
|
||||||
@@ -806,48 +821,61 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
}
|
}
|
||||||
if (forwardIds.isEmpty) return;
|
if (forwardIds.isEmpty) return;
|
||||||
|
|
||||||
|
final resolved = <int, ({String name, String? avatar})>{};
|
||||||
for (final id in forwardIds) {
|
for (final id in forwardIds) {
|
||||||
final name = await messagesModule.searchContactById(id);
|
final name = await messagesModule.searchContactById(id);
|
||||||
final avatar = ContactCache.getAvatar(id);
|
if (name != null) {
|
||||||
if (name != null && mounted) {
|
resolved[id] = (name: name, avatar: ContactCache.getAvatar(id));
|
||||||
setState(() {
|
|
||||||
for (var i = 0; i < _messages.length; i++) {
|
|
||||||
final msg = _messages[i];
|
|
||||||
if (msg.attachments != null) {
|
|
||||||
final newAttaches = msg.attachments!.map((a) {
|
|
||||||
if (a is ForwardedMessageAttachment &&
|
|
||||||
a.originalSenderId == id &&
|
|
||||||
a.originalSenderName == null) {
|
|
||||||
return ForwardedMessageAttachment(
|
|
||||||
originalSenderId: id,
|
|
||||||
originalSenderName: name,
|
|
||||||
originalSenderAvatar: avatar,
|
|
||||||
originalMessageId: a.originalMessageId,
|
|
||||||
originalTime: a.originalTime,
|
|
||||||
originalText: a.originalText,
|
|
||||||
originalChatId: a.originalChatId,
|
|
||||||
originalAttachments: a.originalAttachments,
|
|
||||||
originalContact: a.originalContact,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return a;
|
|
||||||
}).toList();
|
|
||||||
_messages[i] = CachedMessage(
|
|
||||||
id: msg.id,
|
|
||||||
accountId: msg.accountId,
|
|
||||||
chatId: msg.chatId,
|
|
||||||
senderId: msg.senderId,
|
|
||||||
text: msg.text,
|
|
||||||
time: msg.time,
|
|
||||||
status: msg.status,
|
|
||||||
payload: msg.payload,
|
|
||||||
attachments: newAttaches,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (resolved.isEmpty || !mounted) return;
|
||||||
|
|
||||||
|
var anyChanged = false;
|
||||||
|
for (var i = 0; i < _messages.length; i++) {
|
||||||
|
final msg = _messages[i];
|
||||||
|
final attaches = msg.attachments;
|
||||||
|
if (attaches == null) continue;
|
||||||
|
|
||||||
|
var msgChanged = false;
|
||||||
|
final newAttaches = attaches.map((a) {
|
||||||
|
if (a is ForwardedMessageAttachment &&
|
||||||
|
a.originalSenderName == null &&
|
||||||
|
resolved.containsKey(a.originalSenderId)) {
|
||||||
|
final r = resolved[a.originalSenderId]!;
|
||||||
|
msgChanged = true;
|
||||||
|
return ForwardedMessageAttachment(
|
||||||
|
originalSenderId: a.originalSenderId,
|
||||||
|
originalSenderName: r.name,
|
||||||
|
originalSenderAvatar: r.avatar,
|
||||||
|
originalMessageId: a.originalMessageId,
|
||||||
|
originalTime: a.originalTime,
|
||||||
|
originalText: a.originalText,
|
||||||
|
originalChatId: a.originalChatId,
|
||||||
|
originalAttachments: a.originalAttachments,
|
||||||
|
originalContact: a.originalContact,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
if (!msgChanged) continue;
|
||||||
|
anyChanged = true;
|
||||||
|
_messages[i] = CachedMessage(
|
||||||
|
id: msg.id,
|
||||||
|
accountId: msg.accountId,
|
||||||
|
chatId: msg.chatId,
|
||||||
|
senderId: msg.senderId,
|
||||||
|
text: msg.text,
|
||||||
|
time: msg.time,
|
||||||
|
status: msg.status,
|
||||||
|
payload: msg.payload,
|
||||||
|
attachments: newAttaches,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anyChanged) {
|
||||||
|
setState(() => _messagesRevision++);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _scrollToBottom() {
|
void _scrollToBottom() {
|
||||||
@@ -863,7 +891,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<Object> _buildCombinedItems() {
|
List<Object> _buildCombinedItems() {
|
||||||
final key = Object.hashAll(_messages.map(identityHashCode));
|
final key = Object.hash(_messagesRevision, _messages.length);
|
||||||
final cached = _combinedItemsCache;
|
final cached = _combinedItemsCache;
|
||||||
if (cached != null && _combinedItemsKey == key) return cached;
|
if (cached != null && _combinedItemsKey == key) return cached;
|
||||||
|
|
||||||
@@ -906,25 +934,22 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
|
|
||||||
void _onScrollForDate() {
|
void _onScrollForDate() {
|
||||||
if (!_scrollController.hasClients) return;
|
if (!_scrollController.hasClients) return;
|
||||||
final currentOffset = _scrollController.position.pixels;
|
|
||||||
final scrollingUp = currentOffset > _lastScrollOffset;
|
|
||||||
_lastScrollOffset = currentOffset;
|
|
||||||
|
|
||||||
_floatingDateTimer?.cancel();
|
_floatingDateTimer?.cancel();
|
||||||
|
_floatingDateTimer = Timer(const Duration(seconds: 1), () {
|
||||||
if (!scrollingUp) {
|
|
||||||
_floatingDateAnimController.reverse();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_floatingDateTimer = Timer(const Duration(seconds: 2), () {
|
|
||||||
if (mounted) _floatingDateAnimController.reverse();
|
if (mounted) _floatingDateAnimController.reverse();
|
||||||
});
|
});
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => _updateFloatingDate());
|
|
||||||
|
if (_floatingDateScheduled) return;
|
||||||
|
_floatingDateScheduled = true;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_floatingDateScheduled = false;
|
||||||
|
_updateFloatingDate();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateFloatingDate() {
|
void _updateFloatingDate() {
|
||||||
if (!mounted) return;
|
if (!mounted || _separatorKeys.isEmpty) return;
|
||||||
DateTime? result;
|
DateTime? result;
|
||||||
|
|
||||||
final listRenderBox = _listKey.currentContext?.findRenderObject();
|
final listRenderBox = _listKey.currentContext?.findRenderObject();
|
||||||
@@ -1611,6 +1636,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
setState(() {
|
setState(() {
|
||||||
_lastSentId = tempId;
|
_lastSentId = tempId;
|
||||||
_messages.add(msg);
|
_messages.add(msg);
|
||||||
|
_messagesRevision++;
|
||||||
});
|
});
|
||||||
Haptics.send();
|
Haptics.send();
|
||||||
_scrollToBottom();
|
_scrollToBottom();
|
||||||
@@ -1638,6 +1664,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
payload: old.payload,
|
payload: old.payload,
|
||||||
attachments: attachment != null ? [attachment] : old.attachments,
|
attachments: attachment != null ? [attachment] : old.attachments,
|
||||||
);
|
);
|
||||||
|
_messagesRevision++;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1924,7 +1924,7 @@ class _VoiceMessageBubble extends StatefulWidget {
|
|||||||
|
|
||||||
class _VoiceMessageBubbleState extends State<_VoiceMessageBubble> {
|
class _VoiceMessageBubbleState extends State<_VoiceMessageBubble> {
|
||||||
bool _isPlaying = false;
|
bool _isPlaying = false;
|
||||||
double _progress = 0.0;
|
final ValueNotifier<double> _progress = ValueNotifier(0.0);
|
||||||
bool _transcriptionVisible = false;
|
bool _transcriptionVisible = false;
|
||||||
String? _transcriptionText;
|
String? _transcriptionText;
|
||||||
bool _transcriptionLoading = false;
|
bool _transcriptionLoading = false;
|
||||||
@@ -1935,6 +1935,12 @@ class _VoiceMessageBubbleState extends State<_VoiceMessageBubble> {
|
|||||||
_transcriptionText = widget.preloadedText;
|
_transcriptionText = widget.preloadedText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_progress.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
String _formatDuration(int seconds) {
|
String _formatDuration(int seconds) {
|
||||||
final min = seconds ~/ 60;
|
final min = seconds ~/ 60;
|
||||||
final sec = seconds % 60;
|
final sec = seconds % 60;
|
||||||
@@ -2027,18 +2033,14 @@ class _VoiceMessageBubbleState extends State<_VoiceMessageBubble> {
|
|||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTapDown: (details) {
|
onTapDown: (details) {
|
||||||
setState(() {
|
_progress.value =
|
||||||
_progress = (details.localPosition.dx /
|
(details.localPosition.dx / constraints.maxWidth)
|
||||||
constraints.maxWidth)
|
.clamp(0.0, 1.0);
|
||||||
.clamp(0.0, 1.0);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
onHorizontalDragUpdate: (details) {
|
onHorizontalDragUpdate: (details) {
|
||||||
setState(() {
|
_progress.value =
|
||||||
_progress = (details.localPosition.dx /
|
(details.localPosition.dx / constraints.maxWidth)
|
||||||
constraints.maxWidth)
|
.clamp(0.0, 1.0);
|
||||||
.clamp(0.0, 1.0);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 4,
|
height: 4,
|
||||||
@@ -2046,13 +2048,16 @@ class _VoiceMessageBubbleState extends State<_VoiceMessageBubble> {
|
|||||||
color: waveInactiveColor,
|
color: waveInactiveColor,
|
||||||
borderRadius: BorderRadius.circular(2),
|
borderRadius: BorderRadius.circular(2),
|
||||||
),
|
),
|
||||||
child: FractionallySizedBox(
|
child: ValueListenableBuilder<double>(
|
||||||
alignment: Alignment.centerLeft,
|
valueListenable: _progress,
|
||||||
widthFactor: _progress.clamp(0.0, 1.0),
|
builder: (context, progress, _) => FractionallySizedBox(
|
||||||
child: Container(
|
alignment: Alignment.centerLeft,
|
||||||
decoration: BoxDecoration(
|
widthFactor: progress.clamp(0.0, 1.0),
|
||||||
color: waveActiveColor,
|
child: Container(
|
||||||
borderRadius: BorderRadius.circular(2),
|
decoration: BoxDecoration(
|
||||||
|
color: waveActiveColor,
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user