From 15bcc086d753243b18bfa17da449c7202947a511 Mon Sep 17 00:00:00 2001 From: Jganenok Date: Wed, 27 May 2026 23:31:34 +0700 Subject: [PATCH] =?UTF-8?q?=D0=BD=D0=B5=D0=BC=D0=BD=D0=BE=D0=B6=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=BE=D0=BF=D1=82i=D0=BCi=D0=B7=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20=D0=B8=20=D1=87=D0=B8=D1=81=D1=82=D0=BA=D0=B0=20=D0=BA?= =?UTF-8?q?=D0=B0=D0=BB=D0=B0=20=D0=B7=D0=B0=20=D0=BA=D0=BE=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screens/chats/chat_list_screen.dart | 10 +- lib/frontend/screens/chats/chat_screen.dart | 156 +++++++++++++++--- lib/frontend/widgets/adaptive_shell.dart | 11 +- 3 files changed, 148 insertions(+), 29 deletions(-) diff --git a/lib/frontend/screens/chats/chat_list_screen.dart b/lib/frontend/screens/chats/chat_list_screen.dart index 38fb4b9..adbdefa 100644 --- a/lib/frontend/screens/chats/chat_list_screen.dart +++ b/lib/frontend/screens/chats/chat_list_screen.dart @@ -2056,7 +2056,15 @@ class _ChatListScreenState extends State onTap: () { if (_isSelectionMode) { _toggleSelection(id); - } else if (widget.onChatSelected != null) { + return; + } + if (imageUrl.isNotEmpty) { + unawaited(precacheImage( + CachedNetworkImageProvider(imageUrl, maxWidth: 144, maxHeight: 144), + context, + )); + } + if (widget.onChatSelected != null) { widget.onChatSelected!(DesktopChatSelection( chatId: int.parse(id), name: name, diff --git a/lib/frontend/screens/chats/chat_screen.dart b/lib/frontend/screens/chats/chat_screen.dart index 77c61b9..9a2d69d 100644 --- a/lib/frontend/screens/chats/chat_screen.dart +++ b/lib/frontend/screens/chats/chat_screen.dart @@ -97,6 +97,8 @@ class _ChatScreenState extends State String _nextTempId() => 'temp_${++_tempIdCounter}_${DateTime.now().microsecondsSinceEpoch}'; late AnimationController _shimmerController; + Timer? _shimmerStartTimer; + bool _historyKickedOff = false; List _messages = []; int _myId = 0; CachedChat? chat; @@ -117,7 +119,7 @@ class _ChatScreenState extends State _shimmerController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1500), - )..repeat(); + ); _attachAnim = AnimationController( vsync: this, duration: const Duration(milliseconds: 320), @@ -141,9 +143,52 @@ class _ChatScreenState extends State reverseCurve: Curves.easeIn, ); + WidgetsBinding.instance.addPostFrameCallback(_onFirstFrameRendered); + } + + void _onFirstFrameRendered(Duration _) { + if (!mounted) return; + if (widget.embedded) { + _kickoffHistory(); + return; + } + final anim = ModalRoute.of(context)?.animation; + if (anim == null || anim.status == AnimationStatus.completed) { + _kickoffHistory(); + return; + } + Timer? safety; + void onStatus(AnimationStatus status) { + if (status != AnimationStatus.completed) return; + anim.removeStatusListener(onStatus); + safety?.cancel(); + if (!mounted) return; + _kickoffHistory(); + } + anim.addStatusListener(onStatus); + safety = Timer(const Duration(milliseconds: 400), () { + anim.removeStatusListener(onStatus); + if (!mounted) return; + _kickoffHistory(); + }); + } + + void _kickoffHistory() { + if (_historyKickedOff) return; + _historyKickedOff = true; + _shimmerStartTimer = Timer(const Duration(milliseconds: 150), () { + if (!mounted || !_isLoading) return; + _shimmerController.repeat(); + }); _loadHistory(); } + void _onLoadingFinished() { + _shimmerStartTimer?.cancel(); + _shimmerStartTimer = null; + if (_shimmerController.isAnimating) _shimmerController.stop(); + } + Future _loadHistory() async { final activeProfile = await AppDatabase.loadActiveProfile(); _myId = activeProfile?.id ?? 0; @@ -157,19 +202,35 @@ class _ChatScreenState extends State unawaited(_loadOtherPresence()); } - final cachedRows = await AppDatabase.loadMessages( + final firstRows = await AppDatabase.loadMessages( + _myId, + widget.chatId, + limit: 20, + ); + if (mounted && firstRows.isNotEmpty) { + final first = firstRows.reversed + .map((r) => CachedMessage.fromDbRow(r)) + .toList(); + setState(() { + _messages = first; + if (api.state == SessionState.online) { + _isLoading = false; + _onLoadingFinished(); + } + }); + } + + unawaited(_loadRemainingHistory()); + } + + Future _loadRemainingHistory() async { + final fullRows = await AppDatabase.loadMessages( _myId, widget.chatId, limit: 100, ); - if (mounted && cachedRows.isNotEmpty) { - setState(() { - _messages = cachedRows.map((r) => CachedMessage.fromDbRow(r)).toList(); - _messages.sort((a, b) => a.time.compareTo(b.time)); - if (api.state == SessionState.online) { - _isLoading = false; - } - }); + if (mounted && fullRows.length > _messages.length) { + _applyMergedMessages(fullRows); } try { @@ -180,13 +241,7 @@ class _ChatScreenState extends State limit: 100, ); if (mounted) { - setState(() { - _messages = updatedRows - .map((r) => CachedMessage.fromDbRow(r)) - .toList(); - _messages.sort((a, b) => a.time.compareTo(b.time)); - _isLoading = false; - }); + _applyMergedMessages(updatedRows, markLoaded: true); } _loadForwardedSenderNames(); } catch (e) { @@ -194,11 +249,53 @@ class _ChatScreenState extends State if (mounted) { setState(() { _isLoading = false; + _onLoadingFinished(); }); } } } + void _applyMergedMessages( + List> rowsDesc, { + bool markLoaded = false, + }) { + final byId = { + for (final m in _messages) m.id: m, + }; + final merged = []; + for (final row in rowsDesc.reversed) { + final fresh = CachedMessage.fromDbRow(row); + final old = byId[fresh.id]; + merged.add(old != null && _sameMessage(old, fresh) ? old : fresh); + } + + final changed = !_listsEquivalent(_messages, merged); + if (!changed && !markLoaded) return; + setState(() { + if (changed) _messages = merged; + if (markLoaded) { + _isLoading = false; + _onLoadingFinished(); + } + }); + } + + bool _sameMessage(CachedMessage a, CachedMessage b) { + return a.id == b.id && + a.time == b.time && + a.status == b.status && + a.text == b.text && + a.senderId == b.senderId; + } + + bool _listsEquivalent(List a, List 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 dispose() { _messageController.removeListener(_onTextChanged); @@ -221,6 +318,7 @@ class _ChatScreenState extends State _attachAnim.dispose(); _messageController.dispose(); _scrollController.dispose(); + _shimmerStartTimer?.cancel(); _shimmerController.dispose(); super.dispose(); } @@ -906,16 +1004,20 @@ class _ChatScreenState extends State child: bubble, ); - if (message.id == _lastSentId) { - return _SentMessageAnimation( - key: ValueKey('anim_${message.id}'), - onComplete: () { - if (mounted) setState(() => _lastSentId = null); - }, - child: pressable, - ); - } - return pressable; + final Widget child = message.id == _lastSentId + ? _SentMessageAnimation( + key: ValueKey('anim_${message.id}'), + onComplete: () { + if (mounted) setState(() => _lastSentId = null); + }, + child: pressable, + ) + : pressable; + + return RepaintBoundary( + key: ValueKey('msg_${message.id}'), + child: child, + ); }, ), ), diff --git a/lib/frontend/widgets/adaptive_shell.dart b/lib/frontend/widgets/adaptive_shell.dart index c708845..1305c6d 100644 --- a/lib/frontend/widgets/adaptive_shell.dart +++ b/lib/frontend/widgets/adaptive_shell.dart @@ -1,3 +1,6 @@ +import 'dart:async'; + +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -60,6 +63,12 @@ class _AdaptiveShellState extends State { } void _onChatSelected(DesktopChatSelection chat) { + if (chat.imageUrl.isNotEmpty) { + unawaited(precacheImage( + CachedNetworkImageProvider(chat.imageUrl, maxWidth: 144, maxHeight: 144), + context, + )); + } setState(() => _selected = chat); } @@ -84,7 +93,7 @@ class _AdaptiveShellState extends State { return LayoutBuilder( builder: (context, constraints) { if (constraints.maxWidth < _breakpoint) { - return ChatListScreen(onChatSelected: _onChatSelected); + return const ChatListScreen(); } final totalWidth = constraints.maxWidth; final effectiveListWidth = _listWidth.clamp(