diff --git a/lib/backend/modules/messages.dart b/lib/backend/modules/messages.dart index 61d7614..2cc90a7 100644 --- a/lib/backend/modules/messages.dart +++ b/lib/backend/modules/messages.dart @@ -97,7 +97,6 @@ class MessagesModule { final List results = []; final List> rows = []; - // Обрабатываем сообщения "кусочками", чтобы не фризить UI при маппинге большого количества данных for (var i = 0; i < messagesData.length; i++) { final m = messagesData[i]; if (m is! Map) continue; @@ -108,7 +107,6 @@ class MessagesModule { rows.add(msg.toDbRow()); } - // Каждые 20 сообщений даем UI-потоку "дохнуть" и отрисовать кадр if (i > 0 && i % 20 == 0) { await Future.delayed(Duration.zero); } diff --git a/lib/core/protocol/packet.dart b/lib/core/protocol/packet.dart index 18658da..2cb5768 100644 --- a/lib/core/protocol/packet.dart +++ b/lib/core/protocol/packet.dart @@ -3,8 +3,6 @@ import 'dart:isolate'; import 'package:dart_lz4/dart_lz4.dart'; import 'package:msgpack_dart/msgpack_dart.dart' as msgpack; -import '../utils/logger.dart'; - /// ver(1) + cmd(1) + seq(2) + opcode(2) + packedLen(4) = 10 const int headerSize = 10; const int _maxDecompressedSize = 1048576; // 1 MB diff --git a/lib/frontend/screens/chats/chat_list_screen.dart b/lib/frontend/screens/chats/chat_list_screen.dart index cda1a0b..fd1b757 100644 --- a/lib/frontend/screens/chats/chat_list_screen.dart +++ b/lib/frontend/screens/chats/chat_list_screen.dart @@ -23,10 +23,11 @@ class ChatListScreen extends StatefulWidget { } class _ChatListScreenState extends State - with SingleTickerProviderStateMixin { + with TickerProviderStateMixin { String _selectedCategory = 'Все чаты'; int _currentNavIndex = 0; bool _isFabOpen = false; + bool _showCacheWarning = false; late AnimationController _fabController; final Set _selectedChats = {}; final ScrollController _scrollController = ScrollController(); @@ -64,6 +65,9 @@ class _ChatListScreenState extends State }); } + bool _isInitialLoading = true; + late AnimationController _shimmerController; + @override void initState() { super.initState(); @@ -71,11 +75,26 @@ class _ChatListScreenState extends State vsync: this, duration: const Duration(milliseconds: 350), ); + _shimmerController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1500), + )..repeat(); + _scrollController.addListener(_onScroll); _sessionState = api.state; _stateSub = api.stateStream.listen((state) { - if (mounted) setState(() => _sessionState = state); + if (mounted) { + setState(() { + _sessionState = state; + if (state == SessionState.disconnected && _chats.isNotEmpty) { + _showCacheWarning = true; + } + if (state == SessionState.online) { + _showCacheWarning = false; + } + }); + } }); _loadProfile(); @@ -89,6 +108,13 @@ class _ChatListScreenState extends State setState(() { _profile = p; _chats = chats; + _isInitialLoading = false; + }); + } + } else { + if (mounted) { + setState(() { + _isInitialLoading = false; }); } } @@ -102,6 +128,59 @@ class _ChatListScreenState extends State return '$h:$m'; } + Widget _buildChatShimmer() { + final cs = Theme.of(context).colorScheme; + return AnimatedBuilder( + animation: _shimmerController, + builder: (context, child) { + final opacity = 0.3 + 0.3 * sin(_shimmerController.value * pi * 2); + return Opacity( + opacity: opacity, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + child: Row( + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: cs.surfaceContainerHighest, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 120, + height: 14, + decoration: BoxDecoration( + color: cs.surfaceContainerHighest, + borderRadius: BorderRadius.circular(7), + ), + ), + const SizedBox(height: 10), + Container( + width: double.infinity, + height: 10, + decoration: BoxDecoration( + color: cs.surfaceContainerHighest, + borderRadius: BorderRadius.circular(5), + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } + void _onScroll() { if (_scrollController.hasClients) { final double offset = _scrollController.offset; @@ -319,6 +398,49 @@ class _ChatListScreenState extends State ), ), ), + if (_showCacheWarning) + Padding( + padding: const EdgeInsets.fromLTRB( + 20, + 0, + 20, + 8, + ), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + decoration: BoxDecoration( + color: cs.errorContainer.withOpacity( + 0.3, + ), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: cs.error.withOpacity(0.2), + ), + ), + child: Row( + children: [ + Icon( + Symbols.cloud_off, + size: 18, + color: cs.error, + ), + const SizedBox(width: 12), + const Expanded( + child: Text( + 'Ошибка соединения, сейчас вы смотрите КЕШ', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ), Padding( padding: const EdgeInsets.fromLTRB( 20, @@ -418,6 +540,9 @@ class _ChatListScreenState extends State ), SliverList( delegate: SliverChildBuilderDelegate((context, index) { + if (_isInitialLoading) { + return _buildChatShimmer(); + } final chat = _chats[index]; return _buildChatItem( chat.id.toString(), @@ -431,10 +556,10 @@ class _ChatListScreenState extends State unreadCount: chat.unreadCount, isMuted: chat.dontDisturbUntil > 0, ); - }, childCount: _chats.length), + }, childCount: _isInitialLoading ? 10 : _chats.length), ), const SliverPadding( - padding: EdgeInsets.only(bottom: 120), + padding: EdgeInsets.only(bottom: 100), ), ], ), // CustomScrollView @@ -449,7 +574,7 @@ class _ChatListScreenState extends State curve: Curves.easeOutCubic, left: 8, right: 8, - bottom: _isSelectionMode ? -100 : 24.0, + bottom: _isSelectionMode ? -100 : 10.0, child: RepaintBoundary( child: Container( height: 68, @@ -561,7 +686,7 @@ class _ChatListScreenState extends State if (_fabController.value > 0) Positioned( right: 20, - bottom: 110 + 74, + bottom: 90 + 74, child: RepaintBoundary( child: Transform.scale( scale: val, @@ -575,7 +700,7 @@ class _ChatListScreenState extends State ), Positioned( right: 20, - bottom: 110, + bottom: 90, child: FloatingActionButton( onPressed: _toggleFab, backgroundColor: cs.primaryContainer, diff --git a/lib/frontend/screens/chats/chat_screen.dart b/lib/frontend/screens/chats/chat_screen.dart index d5484b2..ab8c985 100644 --- a/lib/frontend/screens/chats/chat_screen.dart +++ b/lib/frontend/screens/chats/chat_screen.dart @@ -1,6 +1,9 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../../../main.dart'; +import '../../../backend/api.dart'; +import '../../../backend/modules/messages.dart'; import '../../../core/storage/app_database.dart'; class ChatScreen extends StatefulWidget { @@ -25,6 +28,7 @@ class _ChatScreenState extends State bool _hasText = false; bool _isLoading = true; late AnimationController _shimmerController; + List _messages = []; @override void initState() { @@ -34,17 +38,45 @@ class _ChatScreenState extends State vsync: this, duration: const Duration(milliseconds: 1500), )..repeat(); + _loadHistory(); } Future _loadHistory() async { + final activeProfile = await AppDatabase.loadActiveProfile(); + final myId = activeProfile?.id ?? 0; + + final cachedRows = await AppDatabase.loadMessages( + myId, + widget.chatId, + limit: 100, + ); + if (mounted && cachedRows.isNotEmpty) { + setState(() { + _messages = cachedRows.map((r) => CachedMessage.fromDbRow(r)).toList(); + if (api.state == SessionState.online) { + _isLoading = false; + } + }); + } + try { - final activeProfile = await AppDatabase.loadActiveProfile(); - final myId = activeProfile?.id ?? 0; await messagesModule.fetchHistory(myId, widget.chatId); + final updatedRows = await AppDatabase.loadMessages( + myId, + widget.chatId, + limit: 100, + ); + if (mounted) { + setState(() { + _messages = updatedRows + .map((r) => CachedMessage.fromDbRow(r)) + .toList(); + _isLoading = false; + }); + } } catch (e) { - debugPrint('Background history fetch failed: $e'); - } finally { + debugPrint('Error fetching history: $e'); if (mounted) { setState(() { _isLoading = false; @@ -148,7 +180,9 @@ class _ChatScreenState extends State body: Column( children: [ Expanded( - child: _isLoading + child: + _isLoading || + (_messages.isEmpty && api.state != SessionState.online) ? _buildShimmerLoading() : const SizedBox.shrink(), ), diff --git a/lib/main.dart b/lib/main.dart index a4dc755..c8fb446 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -91,16 +91,19 @@ class _StartupScreenState extends State<_StartupScreen> { return; } + if (mounted) { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const ChatListScreen()), + ); + } + try { await accountModule.login(accountId: accountId); - if (mounted) { - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (_) => const ChatListScreen()), - ); - } - } catch (_) { - _goToLogin(); + } catch (e) { + debugPrint( + 'Background auto-login failed (safe to ignore if offline): $e', + ); } }