From 60c8d19ed46e5cd7a20b7c390f0c437b25001b65 Mon Sep 17 00:00:00 2001 From: klockky Date: Fri, 3 Apr 2026 22:56:41 +0300 Subject: [PATCH] Add pinned chats header to chat list screen with dynamic story display --- .../screens/chats/chat_list_screen.dart | 608 +++++++++--------- 1 file changed, 292 insertions(+), 316 deletions(-) diff --git a/lib/frontend/screens/chats/chat_list_screen.dart b/lib/frontend/screens/chats/chat_list_screen.dart index d3ee919..273f7b0 100644 --- a/lib/frontend/screens/chats/chat_list_screen.dart +++ b/lib/frontend/screens/chats/chat_list_screen.dart @@ -38,7 +38,8 @@ class _ChatListScreenState extends State double _closeAnimBegin = 0.0; bool _storiesAnimClosing = false; bool _storiesDockedOpen = false; - DateTime _storiesRevealLayoutSettleUntil = DateTime.fromMillisecondsSinceEpoch(0); + DateTime _storiesRevealLayoutSettleUntil = + DateTime.fromMillisecondsSinceEpoch(0); ProfileData? _profile; List _chats = []; SessionState _sessionState = SessionState.disconnected; @@ -363,6 +364,258 @@ class _ChatListScreenState extends State }); } + Widget _buildPinnedChatsHeader(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return ColoredBox( + color: cs.surface, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ClipRect( + clipBehavior: Clip.hardEdge, + child: AnimatedSize( + duration: const Duration(milliseconds: 200), + curve: Curves.easeOutCubic, + alignment: Alignment.topCenter, + child: _shouldCollapseSearch + ? const SizedBox(width: double.infinity, height: 0) + : Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(20, 12, 20, 2), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + if (_pullRatio < 0.8) + Opacity( + opacity: 1.0 - _pullRatio, + child: Container( + width: 50 * (1.0 - _pullRatio), + height: 32, + margin: const EdgeInsets.only(right: 8), + child: Stack( + children: [ + _buildFoldedStory( + 'https://i.pravatar.cc/150?u=dasha', + 0, + ), + _buildFoldedStory( + 'https://i.pravatar.cc/150?u=mastika', + 1, + ), + _buildFoldedStory( + 'https://i.pravatar.cc/150?u=stas', + 2, + ), + ], + ), + ), + ), + Text( + _sessionState == SessionState.online + ? (_profile?.firstName ?? 'Чат') + : 'Подключение...', + style: TextStyle( + color: cs.onSurface, + fontSize: 20, + fontWeight: FontWeight.w600, + fontFamily: 'Outfit', + ), + ), + ], + ), + PopupMenuButton( + icon: Icon( + Symbols.more_vert, + color: cs.outline, + weight: 400, + ), + offset: const Offset(0, 48), + elevation: 4, + color: cs.surfaceContainerHigh, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + itemBuilder: (context) => [ + _buildPopupMenuItem( + 1, + 'Кнопка 1', + Symbols.settings, + ), + _buildPopupMenuItem( + 2, + 'Кнопка 2', + Symbols.notifications, + ), + _buildPopupMenuItem( + 3, + 'Кнопка 3', + Symbols.shield, + ), + _buildPopupMenuItem( + 4, + 'Кнопка 4', + Symbols.info, + ), + ], + ), + ], + ), + ), + SizedBox( + height: 96 * _pullRatio, + child: Opacity( + opacity: _pullRatio, + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric( + horizontal: 20, + ), + children: [ + _buildStoryItem( + 'Даша', + 'https://i.pravatar.cc/150?u=dasha', + true, + ), + _buildStoryItem( + 'Мастика', + 'https://i.pravatar.cc/150?u=mastika', + false, + ), + ], + ), + ), + ), + 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, 2, 20, 8), + child: Container( + height: 44, + decoration: BoxDecoration( + color: cs.surfaceContainerHighest, + borderRadius: BorderRadius.circular(50), + ), + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Icon( + Symbols.search, + color: cs.outline, + size: 20, + weight: 400, + ), + const SizedBox(width: 10), + Expanded( + child: TextField( + style: TextStyle( + color: cs.onSurface, + fontSize: 15, + ), + decoration: InputDecoration( + hintText: 'Поиск', + hintStyle: TextStyle( + color: cs.outline, + fontSize: 15, + ), + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOutCubic, + height: 48, + color: cs.surface, + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + dragDevices: { + ui.PointerDeviceKind.touch, + ui.PointerDeviceKind.mouse, + ui.PointerDeviceKind.trackpad, + }, + ), + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 4, + ), + physics: const BouncingScrollPhysics(), + children: [ + _buildFolderChip('Все чаты'), + const SizedBox(width: 8), + _buildFolderChip('Контакты'), + const SizedBox(width: 8), + _buildFolderChip('Пидоры'), + const SizedBox(width: 8), + _buildFolderChip('Каналы'), + const SizedBox(width: 8), + _buildFolderChip('Группы'), + const SizedBox(width: 8), + _buildFolderChip('Боты'), + const SizedBox(width: 8), + _buildFolderChip('Избранное'), + const SizedBox(width: 8), + _buildFolderChip('Архив'), + ], + ), + ), + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; @@ -391,326 +644,49 @@ class _ChatListScreenState extends State }, child: NotificationListener( onNotification: _onStoriesScrollNotification, - child: CustomScrollView( - controller: _scrollController, - physics: const BouncingScrollPhysics( - parent: AlwaysScrollableScrollPhysics(), - ), - slivers: [ - SliverToBoxAdapter( - child: ClipRect( - clipBehavior: Clip.hardEdge, - child: AnimatedSize( - duration: const Duration(milliseconds: 200), - curve: Curves.easeOutCubic, - alignment: Alignment.topCenter, - child: _shouldCollapseSearch - ? const SizedBox( - width: double.infinity, - height: 0, - ) - : Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB( - 20, - 12, - 20, - 2, - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - if (_pullRatio < 0.8) - Opacity( - opacity: 1.0 - _pullRatio, - child: Container( - width: - 50 * - (1.0 - _pullRatio), - height: 32, - margin: - const EdgeInsets.only( - right: 8, - ), - child: Stack( - children: [ - _buildFoldedStory( - 'https://i.pravatar.cc/150?u=dasha', - 0, - ), - _buildFoldedStory( - 'https://i.pravatar.cc/150?u=mastika', - 1, - ), - _buildFoldedStory( - 'https://i.pravatar.cc/150?u=stas', - 2, - ), - ], - ), - ), - ), - Text( - _sessionState == - SessionState.online - ? (_profile - ?.firstName ?? - 'Чат') - : 'Подключение...', - style: TextStyle( - color: cs.onSurface, - fontSize: 20, - fontWeight: - FontWeight.w600, - fontFamily: 'Outfit', - ), - ), - ], - ), - PopupMenuButton( - icon: Icon( - Symbols.more_vert, - color: cs.outline, - weight: 400, - ), - offset: const Offset(0, 48), - elevation: 4, - color: cs.surfaceContainerHigh, - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(16), - ), - itemBuilder: (context) => [ - _buildPopupMenuItem( - 1, - 'Кнопка 1', - Symbols.settings, - ), - _buildPopupMenuItem( - 2, - 'Кнопка 2', - Symbols.notifications, - ), - _buildPopupMenuItem( - 3, - 'Кнопка 3', - Symbols.shield, - ), - _buildPopupMenuItem( - 4, - 'Кнопка 4', - Symbols.info, - ), - ], - ), - ], - ), - ), - SizedBox( - height: 96 * _pullRatio, - child: Opacity( - opacity: _pullRatio, - child: ListView( - scrollDirection: Axis.horizontal, - padding: - const EdgeInsets.symmetric( - horizontal: 20, - ), - children: [ - _buildStoryItem( - 'Даша', - 'https://i.pravatar.cc/150?u=dasha', - true, - ), - _buildStoryItem( - 'Мастика', - 'https://i.pravatar.cc/150?u=mastika', - false, - ), - ], - ), - ), - ), - 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, - 2, - 20, - 8, - ), - child: Container( - height: 44, - decoration: BoxDecoration( - color: cs.surfaceContainerHighest, - borderRadius: - BorderRadius.circular(50), - ), - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - child: Row( - children: [ - Icon( - Symbols.search, - color: cs.outline, - size: 20, - weight: 400, - ), - const SizedBox(width: 10), - Expanded( - child: TextField( - style: TextStyle( - color: cs.onSurface, - fontSize: 15, - ), - decoration: InputDecoration( - hintText: 'Поиск', - hintStyle: TextStyle( - color: cs.outline, - fontSize: 15, - ), - border: InputBorder.none, - isDense: true, - contentPadding: - EdgeInsets.zero, - ), - ), - ), - ], - ), - ), - ), - ], - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildPinnedChatsHeader(context), + Expanded( + child: CustomScrollView( + controller: _scrollController, + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), ), - ), - ), - SliverPadding( - padding: EdgeInsets.zero, - sliver: SliverToBoxAdapter( - child: AnimatedContainer( - duration: const Duration(milliseconds: 300), - curve: Curves.easeOutCubic, - height: 48, - child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context) - .copyWith( - dragDevices: { - ui.PointerDeviceKind.touch, - ui.PointerDeviceKind.mouse, - ui.PointerDeviceKind.trackpad, - }, - ), - child: ListView( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 4, - ), - physics: const BouncingScrollPhysics(), - children: [ - _buildFolderChip('Все чаты'), - const SizedBox(width: 8), - _buildFolderChip('Контакты'), - const SizedBox(width: 8), - _buildFolderChip('Пидоры'), - const SizedBox(width: 8), - _buildFolderChip('Каналы'), - const SizedBox(width: 8), - _buildFolderChip('Группы'), - const SizedBox(width: 8), - _buildFolderChip('Боты'), - const SizedBox(width: 8), - _buildFolderChip('Избранное'), - const SizedBox(width: 8), - _buildFolderChip('Архив'), - ], + slivers: [ + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + if (_isInitialLoading) { + return _buildChatShimmer(); + } + final chat = _chats[index]; + return _buildChatItem( + chat.id.toString(), + chat.title ?? 'Чат', + chat.lastMsgText ?? '', + _formatTime(chat.lastMsgTime), + (chat.iconUrl != null && + chat.iconUrl!.isNotEmpty) + ? chat.iconUrl! + : '', + isOnline: chat.isOnline, + unreadCount: chat.unreadCount, + isMuted: chat.dontDisturbUntil > 0, + ); + }, + childCount: _isInitialLoading + ? 10 + : _chats.length, ), ), - ), + const SliverPadding( + padding: EdgeInsets.only(bottom: 100), + ), + ], ), ), - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - if (_isInitialLoading) { - return _buildChatShimmer(); - } - final chat = _chats[index]; - return _buildChatItem( - chat.id.toString(), - chat.title ?? 'Чат', - chat.lastMsgText ?? '', - _formatTime(chat.lastMsgTime), - (chat.iconUrl != null && - chat.iconUrl!.isNotEmpty) - ? chat.iconUrl! - : '', - isOnline: chat.isOnline, - unreadCount: chat.unreadCount, - isMuted: chat.dontDisturbUntil > 0, - ); - }, - childCount: _isInitialLoading ? 10 : _chats.length, - ), - ), - const SliverPadding( - padding: EdgeInsets.only(bottom: 100), - ), ], ), ),