From 38a010c486577df44887a214bd8148b1ae1dd277 Mon Sep 17 00:00:00 2001 From: Jganenokk Date: Tue, 14 Jul 2026 12:40:09 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BF=D0=B8=D1=81=D1=8E=D0=BB=D1=8C?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D1=85=D0=BE=D0=B4=20=D0=B2=D0=BD=D0=B8=D0=B7=20=D1=87=D0=B0?= =?UTF-8?q?=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/frontend/screens/chats/chat_screen.dart | 623 +++++++++++++------- 1 file changed, 422 insertions(+), 201 deletions(-) diff --git a/lib/frontend/screens/chats/chat_screen.dart b/lib/frontend/screens/chats/chat_screen.dart index c7de5be..370cf4b 100644 --- a/lib/frontend/screens/chats/chat_screen.dart +++ b/lib/frontend/screens/chats/chat_screen.dart @@ -91,6 +91,7 @@ import '../../widgets/swipe_route.dart'; import '../../widgets/schedule_time_picker.dart'; import '../../widgets/chat_wallpaper_sheet.dart'; import '../../widgets/chat_wallpaper_view.dart'; +import '../../widgets/glossy_pill.dart'; import 'scheduled_messages_screen.dart'; import 'chat_wallpaper_preview_screen.dart'; @@ -321,7 +322,10 @@ class _ChatScreenState extends State _applyReactionInfoToMessage(message.id, result.info); } - void _applyReactionInfoToMessage(String messageId, Map? info) { + void _applyReactionInfoToMessage( + String messageId, + Map? info, + ) { final idx = _messages.indexWhere((m) => m.id == messageId); if (idx == -1) return; final payload = {...?_messages[idx].payload}; @@ -364,7 +368,8 @@ class _ChatScreenState extends State final prev = current?['yourReaction']?.toString(); String? your; if (prev != null && - EmojiKeywordIndex.normalize(prev) == EmojiKeywordIndex.normalize(emoji)) { + EmojiKeywordIndex.normalize(prev) == + EmojiKeywordIndex.normalize(emoji)) { decrement(prev); your = null; } else { @@ -446,6 +451,8 @@ class _ChatScreenState extends State static const double _avgMessageHeight = 72.0; static const double _historyPrefetchExtent = _avgMessageHeight * 8; + static const double _scrollDownRevealExtent = _avgMessageHeight * 30; + static const double _scrollDownTeleportFactor = 2.0; static const double _glossyHeaderHeight = 76.0; static const double _glossySearchHeight = 58.0; static const double _pinnedBannerLift = 6.0; @@ -486,6 +493,11 @@ class _ChatScreenState extends State Timer? _floatingDateTimer; late final AnimationController _floatingDateAnimController; late final CurvedAnimation _floatingDateCurved; + late final AnimationController _scrollDownAnimController; + late final CurvedAnimation _scrollDownCurved; + bool _scrollDownVisible = false; + int _listEpoch = 0; + final List<({String id, double pixels, double alignment})> _returnStack = []; final Map _separatorKeys = {}; String? _lastSentId; final ValueNotifier _otherUnread = ValueNotifier(0); @@ -500,8 +512,10 @@ class _ChatScreenState extends State void _prewarmQuickReactions() { if (!mounted || !RlottieEngine.instance.available) return; - final dpr = - (MediaQuery.maybeOf(context)?.devicePixelRatio ?? 2.0).clamp(1.0, 2.0); + final dpr = (MediaQuery.maybeOf(context)?.devicePixelRatio ?? 2.0).clamp( + 1.0, + 2.0, + ); final px = ((44.0 * dpr).clamp(96.0, 512.0) / 32).ceil() * 32; for (final a in animojiModule.quickAnimojis) { final url = a.lottieUrl; @@ -517,10 +531,12 @@ class _ChatScreenState extends State _chatController.chatId = widget.chatId; _chatController.isMounted = () => mounted; unawaited(PushService.clearChatNotification(widget.chatId)); - unawaited(animojiModule - .ensureLoaded() - .then((_) => _prewarmQuickReactions()) - .catchError((_) {})); + unawaited( + animojiModule + .ensureLoaded() + .then((_) => _prewarmQuickReactions()) + .catchError((_) {}), + ); WidgetsBinding.instance.addObserver(this); chats.chatsChanged.addListener(_onChatsBump); _messageController.addListener(_onTextChanged); @@ -529,6 +545,7 @@ class _ChatScreenState extends State _scrollController.addListener(_recordScrollPixels); _scrollController.addListener(_scheduleReadMarker); _scrollController.addListener(_exitTextSelectionOnScroll); + _scrollController.addListener(_updateScrollDownVisible); AppVisualStyle.current.addListener(_onVisualStyleChanged); AppChatChrome.current.addListener(_onVisualStyleChanged); AppComposerStyle.current.addListener(_onVisualStyleChanged); @@ -595,6 +612,16 @@ class _ChatScreenState extends State curve: Curves.easeOut, reverseCurve: Curves.easeIn, ); + _scrollDownAnimController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 220), + reverseDuration: const Duration(milliseconds: 180), + ); + _scrollDownCurved = CurvedAnimation( + parent: _scrollDownAnimController, + curve: Curves.easeOut, + reverseCurve: Curves.easeIn, + ); unawaited( _fastPreloadCache().then((_) { @@ -1330,7 +1357,9 @@ class _ChatScreenState extends State double? _messageOffsetInList(String messageId) { final listBox = _listKey.currentContext?.findRenderObject(); final box = _keyForMessage(messageId).currentContext?.findRenderObject(); - if (listBox is! RenderBox || box is! RenderBox || !box.attached) return null; + if (listBox is! RenderBox || box is! RenderBox || !box.attached) { + return null; + } return box.localToGlobal(Offset.zero, ancestor: listBox).dy; } @@ -1500,6 +1529,7 @@ class _ChatScreenState extends State _scrollController.removeListener(_recordScrollPixels); _scrollController.removeListener(_scheduleReadMarker); _scrollController.removeListener(_exitTextSelectionOnScroll); + _scrollController.removeListener(_updateScrollDownVisible); _readMarkTimer?.cancel(); AppVisualStyle.current.removeListener(_onVisualStyleChanged); AppChatChrome.current.removeListener(_onVisualStyleChanged); @@ -1511,6 +1541,8 @@ class _ChatScreenState extends State _floatingDateCurved.dispose(); _floatingDateAnimController.dispose(); _floatingDate.dispose(); + _scrollDownCurved.dispose(); + _scrollDownAnimController.dispose(); _hasText.dispose(); _scheduledCount.dispose(); _showAttachmentPanel.removeListener(_onAttachPanelToggle); @@ -1535,8 +1567,9 @@ class _ChatScreenState extends State .removeListener(_recomputeHeaderStatus); PresenceFetch.revision.removeListener(_onPresenceChanged); if (_wallpaperListening) { - ChatWallpaperStore.instance.revision - .removeListener(_applyEffectiveWallpaper); + ChatWallpaperStore.instance.revision.removeListener( + _applyEffectiveWallpaper, + ); } _headerStatusNotifier.dispose(); _otherReadTime.dispose(); @@ -2569,8 +2602,7 @@ class _ChatScreenState extends State child: ChatHeaderRow( glossy: glossy, frosted: - glossy && - chrome == ChatChromeStyle.transparent, + glossy && chrome == ChatChromeStyle.transparent, backdropKey: _pillBackdrop, cs: cs, embedded: widget.embedded, @@ -2730,7 +2762,9 @@ class _ChatScreenState extends State if (!mounted) return; if (!_wallpaperListening) { _wallpaperListening = true; - ChatWallpaperStore.instance.revision.addListener(_applyEffectiveWallpaper); + ChatWallpaperStore.instance.revision.addListener( + _applyEffectiveWallpaper, + ); } _applyEffectiveWallpaper(); } @@ -2738,7 +2772,8 @@ class _ChatScreenState extends State void _applyEffectiveWallpaper() { if (!mounted) return; final store = ChatWallpaperStore.instance; - final wp = store.get(_myId, widget.chatId) ?? + final wp = + store.get(_myId, widget.chatId) ?? store.get(_myId, kGlobalWallpaperChatId); if (!identical(wp, _wallpaper)) setState(() => _wallpaper = wp); } @@ -3550,14 +3585,129 @@ class _ChatScreenState extends State } void _scrollToBottom() { + _returnStack.clear(); WidgetsBinding.instance.addPostFrameCallback((_) { - if (_scrollController.hasClients) { - _scrollController.animateTo( - _scrollController.position.minScrollExtent, - duration: const Duration(milliseconds: 300), - curve: Curves.easeOut, - ); + if (!_scrollController.hasClients) return; + final pos = _scrollController.position; + final runway = pos.viewportDimension; + final teleport = pos.pixels > runway * _scrollDownTeleportFactor; + if (teleport) { + _pinnedMessageId = null; + _listEpoch++; + _jumpCacheExtent.value = _jumpCacheExtentPx; + _bumpMessages(); + _scrollController.jumpTo(runway); } + unawaited( + _scrollController + .animateTo( + pos.minScrollExtent, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ) + .whenComplete(() { + if (!teleport) return; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) _jumpCacheExtent.value = null; + }); + }), + ); + }); + } + + void _updateScrollDownVisible() { + if (!_scrollController.hasClients) return; + final pos = _scrollController.position; + if (_returnStack.isNotEmpty && + _isNearBottom() && + pos.userScrollDirection != ScrollDirection.idle) { + _returnStack.clear(); + } + final show = + pos.pixels >= _scrollDownRevealExtent || _returnStack.isNotEmpty; + if (show == _scrollDownVisible) return; + _scrollDownVisible = show; + if (show) { + _scrollDownAnimController.forward(); + } else { + _scrollDownAnimController.reverse(); + } + } + + void _pushReturnAnchor(String messageId) { + if (!_scrollController.hasClients) return; + final listBox = _listKey.currentContext?.findRenderObject(); + final dy = _messageOffsetInList(messageId); + final viewportH = listBox is RenderBox ? listBox.size.height : 0.0; + final alignment = viewportH > 0 && dy != null + ? (dy / viewportH).clamp(0.0, 1.0) + : 0.5; + _returnStack.add(( + id: messageId, + pixels: _scrollController.position.pixels, + alignment: alignment.toDouble(), + )); + if (!_scrollDownVisible) { + _scrollDownVisible = true; + _scrollDownAnimController.forward(); + } + } + + void _onScrollDownTap() { + if (!_scrollController.hasClients) { + _scrollToBottom(); + return; + } + final pixels = _scrollController.position.pixels; + while (_returnStack.isNotEmpty) { + final anchor = _returnStack.removeLast(); + if (anchor.pixels < pixels && _messages.any((m) => m.id == anchor.id)) { + unawaited(_returnToAnchor(anchor)); + return; + } + } + _scrollToBottom(); + } + + Future _returnToAnchor( + ({String id, double pixels, double alignment}) anchor, + ) async { + final pos = _scrollController.position; + final runway = pos.viewportDimension; + final target = anchor.pixels.clamp( + pos.minScrollExtent, + pos.maxScrollExtent, + ); + final distance = (pos.pixels - target).abs(); + final far = distance > runway * _scrollDownTeleportFactor; + + if (!far) { + await _scrollController.animateTo( + target, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + if (!mounted) return; + await _scrollToMessagePrecise(anchor.id, alignment: anchor.alignment); + return; + } + + _jumpCacheExtent.value = _jumpCacheExtentPx; + if (target + runway < distance) { + _listEpoch++; + _bumpMessages(); + _scrollController.jumpTo(target + runway); + await _scrollController.animateTo( + target, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + if (!mounted) return; + } + await _scrollToMessagePrecise(anchor.id, alignment: anchor.alignment); + if (!mounted) return; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) _jumpCacheExtent.value = null; }); } @@ -3600,13 +3750,15 @@ class _ChatScreenState extends State ); } - void _jumpToMessage(String messageId) { + void _jumpToMessage(String messageId, {String? fromId}) { final index = _messages.indexWhere((m) => m.id == messageId); if (index == -1) { showCustomNotification(context, 'Сообщение не загружено'); return; } + if (fromId != null) _pushReturnAnchor(fromId); + final key = _keyForMessage(messageId); final ctx = key.currentContext; if (ctx != null) { @@ -3616,6 +3768,8 @@ class _ChatScreenState extends State curve: Curves.easeOut, alignment: 0.4, ); + } else { + unawaited(_scrollToMessagePrecise(messageId, alignment: 0.4)); } _highlightTimer?.cancel(); @@ -4146,7 +4300,8 @@ class _ChatScreenState extends State _textSelection, ]), builder: (context, child) => PopScope( - canPop: _selectedIds.value.isEmpty && + canPop: + _selectedIds.value.isEmpty && !_search.searchMode.value && _textSelection.value == null, onPopInvokedWithResult: (didPop, _) { @@ -4467,185 +4622,211 @@ class _ChatScreenState extends State jumpExtent != null && jumpExtent < userCacheExtent ? jumpExtent : userCacheExtent; - return ListView.builder( + return CustomScrollView( controller: _scrollController, reverse: true, - padding: _messagesListPadding(context), cacheExtent: cacheExtent, - itemCount: items.length + 1 + (_isLoadingMore ? 1 : 0), - itemBuilder: (context, index) { - if (index == 0) { - return ValueListenableBuilder( - valueListenable: _composerHeight, - builder: (context, height, _) => - SizedBox(height: _composerUnderlap ? height : 0), - ); - } - if (index > items.length) { - return _buildLoadMoreIndicator(); - } - final item = items[items.length - index]; + slivers: [ + SliverPadding( + padding: _messagesListPadding(context), + sliver: SliverList( + key: ValueKey(_listEpoch), + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index == 0) { + return ValueListenableBuilder( + valueListenable: _composerHeight, + builder: (context, height, _) => SizedBox( + height: _composerUnderlap ? height : 0, + ), + ); + } + if (index > items.length) { + return _buildLoadMoreIndicator(); + } + final item = items[items.length - index]; - if (item is _DateSeparatorItem) { - return _buildDateSeparatorWidget( - context, - item.date, - key: item.key, - ); - } - - if (item is _UnreadSeparatorItem) { - return _buildUnreadSeparatorWidget(context); - } - - final msgItem = item as _MessageItem; - final message = msgItem.message; - final msgIndex = msgItem.index; - final isMe = message.senderId == _myId; - final prevMessage = msgIndex > 0 - ? _messages[msgIndex - 1] - : null; - final nextMessage = msgIndex < _messages.length - 1 - ? _messages[msgIndex + 1] - : null; - - final bubble = MessageBubble( - message: message, - isMe: isMe, - myId: _myId, - prevMessage: prevMessage, - nextMessage: nextMessage, - chatType: chat?.type ?? 'CHAT', - overrideStatus: _effectiveStatus(message), - otherReadTime: _otherReadTime, - reactionsListenable: _reactionNotifierFor(message), - uploadProgress: _photoProgressFor(message), - onReplyTap: _jumpToMessage, - onAvatarTap: _openSenderProfile, - onStickerTap: _openStickerPack, - onReactionTap: message.isControl - ? null - : (emoji) => _reactToMessage(message, emoji), - peerName: widget.name, - peerAvatarUrl: widget.imageUrl, - textSelection: _textSelection, - onExitTextSelection: _exitTextSelection, - ); - - final canReport = !isMe && !message.isControl; - final reportTypeId = _complaintTypeId( - chat?.type ?? widget.chatType, - ); - - final pressable = _SelectableMessageRow( - message: message, - isMe: isMe, - selectedIds: _selectedIds, - selectionAnim: _selectionAnim, - isSelectionActive: () => _selectionMode, - onToggleSelection: () => _toggleSelection(message), - onEnterSelection: () => _enterSelection(message), - onStartTextSelection: (pos) => - _startTextSelection(message, pos), - onDelete: () => _confirmDeleteMessage(message, isMe), - onEdit: _canEditMessage(message) - ? () => _startEditMessage(message) - : null, - onReply: message.isControl - ? null - : () => _startReply(message), - onForward: message.isControl - ? null - : () => _forwardMessages([message]), - onMarkUnread: message.isControl - ? null - : () => _markMessageUnread(message), - onPin: _canPinMessage(message) - ? () => _togglePinMessage(message) - : null, - isPinned: () => - chat?.pinnedMsgId == int.tryParse(message.id), - loadReadBy: _canShowReadBy(message) - ? () => _loadReadBy(message) - : null, - onReaderTap: _openSenderProfile, - loadReportReasons: canReport - ? () => _loadReportReasons(reportTypeId) - : null, - onReport: canReport - ? (reasonId) => _reportMessage( - message, - reportTypeId, - reasonId, - ) - : null, - onReact: message.isControl - ? null - : (emoji) => _reactToMessage(message, emoji), - reactions: _reactionNotifierFor(message), - child: bubble, - ); - - final isChannel = - (chat?.type ?? widget.chatType) == 'CHANNEL'; - final swipeable = (message.isControl || isChannel) - ? pressable - : _SwipeToReply( - isMe: isMe, - onReply: () => _startReply(message), - child: pressable, - ); - - final Widget child; - if (_deletingIds.contains(message.id)) { - child = _DeletingMessageAnimation( - key: ValueKey('del_${message.id}'), - onComplete: () => _finalizeDelete(message.id), - child: IgnorePointer(child: swipeable), - ); - } else if (message.id == _lastSentId) { - child = _SentMessageAnimation( - key: ValueKey('anim_${message.id}'), - onComplete: () { - if (mounted) { - _lastSentId = null; - _bumpMessages(); - } - }, - child: swipeable, - ); - } else { - child = swipeable; - } - - final highlightable = ValueListenableBuilder( - valueListenable: _highlightMessageId, - builder: (context, hl, c) => AnimatedContainer( - duration: const Duration(milliseconds: 250), - color: hl == message.id - ? Theme.of( + if (item is _DateSeparatorItem) { + return _buildDateSeparatorWidget( context, - ).colorScheme.primary.withValues(alpha: 0.12) - : Colors.transparent, - child: c, - ), - child: child, - ); + item.date, + key: item.key, + ); + } - final builtItem = RepaintBoundary( - key: ValueKey('msg_${message.id}'), - child: KeyedSubtree( - key: _keyForMessage(message.id), - child: highlightable, + if (item is _UnreadSeparatorItem) { + return _buildUnreadSeparatorWidget(context); + } + + final msgItem = item as _MessageItem; + final message = msgItem.message; + final msgIndex = msgItem.index; + final isMe = message.senderId == _myId; + final prevMessage = msgIndex > 0 + ? _messages[msgIndex - 1] + : null; + final nextMessage = + msgIndex < _messages.length - 1 + ? _messages[msgIndex + 1] + : null; + + final bubble = MessageBubble( + message: message, + isMe: isMe, + myId: _myId, + prevMessage: prevMessage, + nextMessage: nextMessage, + chatType: chat?.type ?? 'CHAT', + overrideStatus: _effectiveStatus(message), + otherReadTime: _otherReadTime, + reactionsListenable: _reactionNotifierFor( + message, + ), + uploadProgress: _photoProgressFor(message), + onReplyTap: (id) => + _jumpToMessage(id, fromId: message.id), + onAvatarTap: _openSenderProfile, + onStickerTap: _openStickerPack, + onReactionTap: message.isControl + ? null + : (emoji) => + _reactToMessage(message, emoji), + peerName: widget.name, + peerAvatarUrl: widget.imageUrl, + textSelection: _textSelection, + onExitTextSelection: _exitTextSelection, + ); + + final canReport = !isMe && !message.isControl; + final reportTypeId = _complaintTypeId( + chat?.type ?? widget.chatType, + ); + + final pressable = _SelectableMessageRow( + message: message, + isMe: isMe, + selectedIds: _selectedIds, + selectionAnim: _selectionAnim, + isSelectionActive: () => _selectionMode, + onToggleSelection: () => + _toggleSelection(message), + onEnterSelection: () => + _enterSelection(message), + onStartTextSelection: (pos) => + _startTextSelection(message, pos), + onDelete: () => + _confirmDeleteMessage(message, isMe), + onEdit: _canEditMessage(message) + ? () => _startEditMessage(message) + : null, + onReply: message.isControl + ? null + : () => _startReply(message), + onForward: message.isControl + ? null + : () => _forwardMessages([message]), + onMarkUnread: message.isControl + ? null + : () => _markMessageUnread(message), + onPin: _canPinMessage(message) + ? () => _togglePinMessage(message) + : null, + isPinned: () => + chat?.pinnedMsgId == + int.tryParse(message.id), + loadReadBy: _canShowReadBy(message) + ? () => _loadReadBy(message) + : null, + onReaderTap: _openSenderProfile, + loadReportReasons: canReport + ? () => _loadReportReasons(reportTypeId) + : null, + onReport: canReport + ? (reasonId) => _reportMessage( + message, + reportTypeId, + reasonId, + ) + : null, + onReact: message.isControl + ? null + : (emoji) => + _reactToMessage(message, emoji), + reactions: _reactionNotifierFor(message), + child: bubble, + ); + + final isChannel = + (chat?.type ?? widget.chatType) == 'CHANNEL'; + final swipeable = (message.isControl || isChannel) + ? pressable + : _SwipeToReply( + isMe: isMe, + onReply: () => _startReply(message), + child: pressable, + ); + + final Widget child; + if (_deletingIds.contains(message.id)) { + child = _DeletingMessageAnimation( + key: ValueKey('del_${message.id}'), + onComplete: () => _finalizeDelete(message.id), + child: IgnorePointer(child: swipeable), + ); + } else if (message.id == _lastSentId) { + child = _SentMessageAnimation( + key: ValueKey('anim_${message.id}'), + onComplete: () { + if (mounted) { + _lastSentId = null; + _bumpMessages(); + } + }, + child: swipeable, + ); + } else { + child = swipeable; + } + + final highlightable = + ValueListenableBuilder( + valueListenable: _highlightMessageId, + builder: (context, hl, c) => + AnimatedContainer( + duration: const Duration( + milliseconds: 250, + ), + color: hl == message.id + ? Theme.of(context) + .colorScheme + .primary + .withValues(alpha: 0.12) + : Colors.transparent, + child: c, + ), + child: child, + ); + + final builtItem = RepaintBoundary( + key: ValueKey('msg_${message.id}'), + child: KeyedSubtree( + key: _keyForMessage(message.id), + child: highlightable, + ), + ); + return message.id == _prank.bubbleId + ? KeyedSubtree( + key: _prank.bubbleKey, + child: builtItem, + ) + : builtItem; + }, + childCount: + items.length + 1 + (_isLoadingMore ? 1 : 0), + ), ), - ); - return message.id == _prank.bubbleId - ? KeyedSubtree( - key: _prank.bubbleKey, - child: builtItem, - ) - : builtItem; - }, + ), + ], ); }, ), @@ -4685,10 +4866,54 @@ class _ChatScreenState extends State ), ), ), + _buildScrollDownButton(), ], ); } + Widget _buildScrollDownButton() { + final cs = Theme.of(context).colorScheme; + final frosted = _effectiveChrome == ChatChromeStyle.transparent; + return ValueListenableBuilder( + valueListenable: _composerHeight, + builder: (context, height, child) => Positioned( + right: 16, + bottom: (_composerUnderlap ? height : 0) + 12, + child: child!, + ), + child: AnimatedBuilder( + animation: _scrollDownCurved, + builder: (context, child) { + final t = _scrollDownCurved.value; + if (t == 0) return const SizedBox.shrink(); + return Opacity( + opacity: t, + child: Transform.scale(scale: 0.82 + 0.18 * t, child: child), + ); + }, + child: SizedBox( + width: 46, + height: 46, + child: GlossyPill( + color: frosted ? AppFrost.pillTint(cs) : null, + blurSigma: frosted ? AppFrost.sigma : null, + backdropKey: _pillBackdrop, + elevated: true, + onTap: _onScrollDownTap, + child: Center( + child: Icon( + Symbols.keyboard_arrow_down, + color: cs.onSurface, + weight: 500, + size: 26, + ), + ), + ), + ), + ), + ); + } + Uint8List _buildWave(List amps, {int bars = 80}) { final out = Uint8List(bars); if (amps.isEmpty) return out; @@ -5770,9 +5995,7 @@ class _PinnedMessageBanner extends StatelessWidget { if (frosted) { return ClipRRect( - borderRadius: floating - ? BorderRadius.circular(16) - : BorderRadius.zero, + borderRadius: floating ? BorderRadius.circular(16) : BorderRadius.zero, child: BackdropFilter( filter: ui.ImageFilter.blur( sigmaX: AppFrost.sigma, @@ -5780,9 +6003,7 @@ class _PinnedMessageBanner extends StatelessWidget { ), backdropGroupKey: backdropKey, child: DecoratedBox( - decoration: BoxDecoration( - border: floating ? null : bottomBorder, - ), + decoration: BoxDecoration(border: floating ? null : bottomBorder), child: content, ), ),