From 505595889fecfeefd13143c655c14c59e9f1a1c4 Mon Sep 17 00:00:00 2001 From: Aleksei Savin Date: Sat, 13 Jun 2026 20:28:43 +0300 Subject: [PATCH 01/12] feat: add iPad mini avatar rail layout Add an iPad compact sidebar state that collapses the chat list to an avatar rail, with drag/tap restoration to the regular master list. Hide compact-mode chrome that does not fit the rail and clamp narrow chat-list measurements to avoid invalid AsyncDisplayKit geometry. --- .../Sources/ChatListControllerNode.swift | 21 +++-- .../Sources/Node/ChatListItem.swift | 37 ++++++--- .../Sources/Node/ChatListNode.swift | 2 +- .../Navigation/NavigationSplitContainer.swift | 77 ++++++++++++++++++- .../Sources/TabBarContollerNode.swift | 6 +- 5 files changed, 123 insertions(+), 20 deletions(-) diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 61cbefe701d..c31bcf8a325 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1009,7 +1009,6 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele public func update(layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, originalNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat, insets: UIEdgeInsets, isReorderingFilters: Bool, isEditing: Bool, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat, storiesInset: CGFloat, transition: ContainedViewLayoutTransition) { self.validLayout = (layout, navigationBarHeight, visualNavigationHeight, originalNavigationHeight, cleanNavigationBarHeight, insets, isReorderingFilters, isEditing, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) - self._validLayoutReady.set(.single(true)) transition.updateAlpha(node: self, alpha: isReorderingFilters ? 0.5 : 1.0) @@ -1024,7 +1023,6 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele self.panRecognizer?.isEnabled = !isEditing transition.updateFrame(layer: self.leftSeparatorLayer, frame: CGRect(origin: CGPoint(x: -UIScreenPixel, y: 0.0), size: CGSize(width: UIScreenPixel, height: layout.size.height))) - if let selectedIndex = self.availableFilters.firstIndex(where: { $0.id == self.selectedId }) { var validNodeIds: [ChatListFilterTabEntryId] = [] for i in max(0, selectedIndex - 1) ... min(self.availableFilters.count - 1, selectedIndex + 1) { @@ -1229,7 +1227,6 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { } self.addSubnode(self.debugListView) - filterBecameEmpty = { [weak self] _ in guard let strongSelf = self else { return @@ -1504,7 +1501,8 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { var navigationHeaderPanels: AnyComponent? if self.controller?.tabContainerData != nil || !panels.isEmpty { var tabs: AnyComponent? - if let tabContainerData = self.controller?.tabContainerData, tabContainerData.0.count > 1 { + let isDesktopLikeCompactSidebar = layout.deviceMetrics.type == .tablet && layout.size.width <= 160.0 + if let tabContainerData = self.controller?.tabContainerData, tabContainerData.0.count > 1, !isDesktopLikeCompactSidebar { let folderFilterIndex: (ChatListFilterTabEntryId, [ChatListFilterTabEntry]) -> Int? = { id, entries in var index = 0 for entry in entries { @@ -1658,6 +1656,15 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { } } + let isDesktopLikeCompactSidebar = layout.deviceMetrics.type == .tablet && layout.size.width <= 160.0 + if isDesktopLikeCompactSidebar { + if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { + navigationBarComponentView.isHidden = true + navigationBarComponentView.isUserInteractionEnabled = false + } + return (0.0, 0.0) + } + let navigationBarSize = self.navigationBarView.update( transition: transition, component: AnyComponent(ChatListNavigationBar( @@ -1715,6 +1722,8 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { containerSize: layout.size ) if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { + navigationBarComponentView.isHidden = false + navigationBarComponentView.isUserInteractionEnabled = true if deferScrollApplication { navigationBarComponentView.deferScrollApplication = true } @@ -1950,7 +1959,6 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { mainInsets.top = visualNavigationHeight } self.mainContainerNode.update(layout: layout, navigationBarHeight: mainNavigationBarHeight, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: navigationBarHeight, cleanNavigationBarHeight: cleanMainNavigationBarHeight, insets: mainInsets, isReorderingFilters: self.isReorderingFilters, isEditing: self.isEditing, inlineNavigationLocation: self.inlineStackContainerNode?.location, inlineNavigationTransitionFraction: self.inlineStackContainerTransitionFraction, storiesInset: storiesInset, transition: transition) - if let inlineStackContainerNode = self.inlineStackContainerNode { var inlineStackContainerNodeTransition = transition var animateIn = false @@ -2006,6 +2014,9 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { guard let (containerLayout, _, _, cleanNavigationBarHeight, _) = self.containerLayout, self.searchDisplayController == nil else { return nil } + if containerLayout.deviceMetrics.type == .tablet && containerLayout.size.width <= 160.0 { + return nil + } let effectiveLocation = self.inlineStackContainerNode?.location ?? self.location diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 67257066fe6..1d0b048dbb9 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -2475,6 +2475,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { let currentAvatarBadgeCleanBackgroundImage: UIImage? = PresentationResourcesChatList.badgeBackgroundBorder(item.presentationData.theme, diameter: avatarBadgeDiameter + 4.0) let leftInset: CGFloat = params.leftInset + avatarLeftInset + let avatarRailMode = useChatListLayout && params.width <= 120.0 && !item.editing && !item.interaction.isInlineMode enum ContentData { case chat(itemPeer: EngineRenderedPeer, threadInfo: ChatListItemContent.ThreadInfo?, peer: EnginePeer?, hideAuthor: Bool, messageText: String, messageEntities: [MessageTextEntity], spoilers: [NSRange]?, customEmojiRanges: [(NSRange, ChatTextInputTextCustomEmojiAttribute)]?) @@ -3518,7 +3519,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { let layoutOffset: CGFloat = 0.0 - let rawContentWidth = params.width - leftInset - params.rightInset - 18.0 - editingOffset + let rawContentWidth = max(1.0, params.width - leftInset - params.rightInset - 18.0 - editingOffset) let (dateLayout, dateApply) = dateLayout(TextNodeLayoutArguments(attributedString: dateAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) @@ -3647,12 +3648,12 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { textCutout = TextNodeCutout(topLeft: textLeftCutout.isZero ? nil : CGSize(width: textLeftCutout, height: 10.0), topRight: nil, bottomRight: textBottomRightCutout.isZero ? nil : CGSize(width: textBottomRightCutout, height: 10.0)) } - var textMaxWidth = rawContentWidth - badgeSize + var textMaxWidth = max(1.0, rawContentWidth - badgeSize) var textArrowImage: UIImage? if isFirstForumThreadSelectable { textArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme) - textMaxWidth -= 18.0 + textMaxWidth = max(1.0, textMaxWidth - 18.0) } let textLineSpacing: CGFloat = min(0.2, item.presentationData.fontSize.itemListBaseFontSize * 0.2 / 17.0) @@ -3685,7 +3686,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { titleAttributedString = NSAttributedString(string: " ", font: titleFont, textColor: theme.titleColor) } - var titleRectWidth = rawContentWidth - dateLayout.size.width - 10.0 - statusWidth - titleIconsWidth + var titleRectWidth = max(1.0, rawContentWidth - dateLayout.size.width - 10.0 - statusWidth - titleIconsWidth) var titleCutout: TextNodeCutout? if !titleLeftCutout.isZero { titleCutout = TextNodeCutout(topLeft: CGSize(width: titleLeftCutout, height: 10.0), topRight: nil, bottomRight: nil) @@ -3695,7 +3696,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { if let titleBadgeText { let titleBadgeLayoutAndApplyValue = titleBadgeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleBadgeText, font: Font.semibold(11.0), textColor: theme.titleColor.withMultipliedAlpha(0.4)), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: titleRectWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) titleBadgeLayoutAndApply = titleBadgeLayoutAndApplyValue - titleRectWidth = max(10.0, titleRectWidth - titleBadgeLayoutAndApplyValue.0.size.width - 8.0) + titleRectWidth = max(1.0, titleRectWidth - titleBadgeLayoutAndApplyValue.0.size.width - 8.0) } let (titleLayout, titleApply) = titleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: maxTitleLines, truncationType: .end, constrainedSize: CGSize(width: titleRectWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: titleCutout, insets: UIEdgeInsets())) @@ -3709,11 +3710,11 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { chatPeerId = peerId } if let inputActivities = inputActivities, !inputActivities.isEmpty, let chatPeerId { - let (size, apply) = inputActivitiesLayout(CGSize(width: rawContentWidth - badgeSize, height: 40.0), item.presentationData, item.presentationData.theme.chatList.messageTextColor, chatPeerId, inputActivities) + let (size, apply) = inputActivitiesLayout(CGSize(width: max(1.0, rawContentWidth - badgeSize), height: 40.0), item.presentationData, item.presentationData.theme.chatList.messageTextColor, chatPeerId, inputActivities) inputActivitiesSize = size inputActivitiesApply = apply } else { - let (size, apply) = inputActivitiesLayout(CGSize(width: rawContentWidth - badgeSize, height: 40.0), item.presentationData, item.presentationData.theme.chatList.messageTextColor, nil, []) + let (size, apply) = inputActivitiesLayout(CGSize(width: max(1.0, rawContentWidth - badgeSize), height: 40.0), item.presentationData, item.presentationData.theme.chatList.messageTextColor, nil, []) inputActivitiesSize = size inputActivitiesApply = apply } @@ -3941,6 +3942,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { if useChatListLayout { mainContentFrame = CGRect(origin: CGPoint(x: leftInset - 2.0, y: 0.0), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height)) mainContentBoundsOffset = mainContentFrame.origin.x + if avatarRailMode { + mainContentAlpha = 0.0 + } if let inlineNavigationLocation = item.interaction.inlineNavigationLocation { mainContentAlpha = 1.0 - inlineNavigationLocation.progress @@ -4025,7 +4029,12 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { let contentRect = rawContentRect.offsetBy(dx: editingOffset + leftInset + revealOffset, dy: 0.0) - let avatarFrame = CGRect(origin: CGPoint(x: leftInset - avatarLeftInset + editingOffset + 10.0 + 6.0 + revealOffset, y: floor((itemHeight - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter)) + let avatarFrame: CGRect + if avatarRailMode { + avatarFrame = CGRect(origin: CGPoint(x: floor((params.width - avatarDiameter) / 2.0) + revealOffset, y: floor((itemHeight - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter)) + } else { + avatarFrame = CGRect(origin: CGPoint(x: leftInset - avatarLeftInset + editingOffset + 10.0 + 6.0 + revealOffset, y: floor((itemHeight - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter)) + } var avatarScaleOffset: CGFloat = 0.0 var avatarScale: CGFloat = 1.0 if let inlineNavigationLocation = item.interaction.inlineNavigationLocation { @@ -4077,7 +4086,15 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } } + let avatarBadgeProgress: CGFloat? if let inlineNavigationLocation = item.interaction.inlineNavigationLocation, badgeContent != .none { + avatarBadgeProgress = inlineNavigationLocation.progress + } else if avatarRailMode, badgeContent != .none { + avatarBadgeProgress = 1.0 + } else { + avatarBadgeProgress = nil + } + if let avatarBadgeProgress = avatarBadgeProgress { var animateIn = false let avatarBadgeBackground: ASImageNode @@ -4117,8 +4134,8 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { ContainedViewLayoutTransition.immediate.updateSublayerTransformScale(node: avatarBadgeNode, scale: 0.00001) ContainedViewLayoutTransition.immediate.updateTransformScale(layer: avatarBadgeBackground.layer, scale: 0.00001) } - transition.updateSublayerTransformScale(node: avatarBadgeNode, scale: max(0.00001, inlineNavigationLocation.progress)) - transition.updateTransformScale(layer: avatarBadgeBackground.layer, scale: max(0.00001, inlineNavigationLocation.progress)) + transition.updateSublayerTransformScale(node: avatarBadgeNode, scale: max(0.00001, avatarBadgeProgress)) + transition.updateTransformScale(layer: avatarBadgeBackground.layer, scale: max(0.00001, avatarBadgeProgress)) } else if let avatarBadgeNode = strongSelf.avatarBadgeNode { strongSelf.avatarBadgeNode = nil transition.updateSublayerTransformScale(node: avatarBadgeNode, scale: 0.00001, completion: { [weak avatarBadgeNode] _ in diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 2dc44bb69bf..ecf1e5d2e41 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -1395,7 +1395,7 @@ public final class ChatListNode: ListViewImpl { self.keepMinimalScrollHeightWithTopInset = self.scrollHeightTopInset let nodeInteraction = ChatListNodeInteraction(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: { [weak self] in - if let strongSelf = self, let activateSearch = strongSelf.activateSearch { + if let strongSelf = self, strongSelf.bounds.width > 120.0, let activateSearch = strongSelf.activateSearch { activateSearch() } }, peerSelected: { [weak self] peer, _, threadId, promoInfo, _ in diff --git a/submodules/Display/Source/Navigation/NavigationSplitContainer.swift b/submodules/Display/Source/Navigation/NavigationSplitContainer.swift index b3900d15c51..1df11d672a7 100644 --- a/submodules/Display/Source/Navigation/NavigationSplitContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationSplitContainer.swift @@ -9,11 +9,21 @@ enum NavigationSplitContainerScrollToTop { } final class NavigationSplitContainer: ASDisplayNode { + private enum Metrics { + static let regularMasterMinWidth: CGFloat = 320.0 + static let compactMasterWidth: CGFloat = 96.0 + static let avatarRailMaximumContainerWidth: CGFloat = 1133.0 + } + private var theme: NavigationControllerTheme private let masterContainer: NavigationContainer private let detailContainer: NavigationContainer private let separator: ASDisplayNode + private let resizeHandle: ASDisplayNode + private var forceRegularMasterWidth: Bool = UserDefaults.standard.bool(forKey: "NavigationSplitContainer.forceRegularMasterWidth") + private var currentLayout: ContainerViewLayout? + private var currentDetailsPlaceholderNode: NavigationDetailsPlaceholderNode? private(set) var masterControllers: [ViewController] = [] private(set) var detailControllers: [ViewController] = [] @@ -48,12 +58,28 @@ final class NavigationSplitContainer: ASDisplayNode { self.separator = ASDisplayNode() self.separator.backgroundColor = theme.navigationBar.separatorColor + + self.resizeHandle = ASDisplayNode() + self.resizeHandle.backgroundColor = .clear + self.resizeHandle.isUserInteractionEnabled = true super.init() self.addSubnode(self.masterContainer) self.addSubnode(self.detailContainer) self.addSubnode(self.separator) + self.addSubnode(self.resizeHandle) + } + + override func didLoad() { + super.didLoad() + + let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.handleResizePan(_:))) + self.resizeHandle.view.addGestureRecognizer(panGesture) + + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.handleResizeTap(_:))) + tapGesture.numberOfTapsRequired = 1 + self.resizeHandle.view.addGestureRecognizer(tapGesture) } func hasNonReadyControllers() -> Bool { @@ -71,7 +97,7 @@ final class NavigationSplitContainer: ASDisplayNode { } func scrollToTopProxyFrames(layout: ContainerViewLayout) -> (master: CGRect, detail: CGRect) { - let masterWidth: CGFloat = min(max(320.0, floor(layout.size.width / 3.0)), floor(layout.size.width / 2.0)) + let masterWidth = self.masterWidth(for: layout) let detailWidth = layout.size.width - masterWidth let scrollToTopHeight = max(layout.statusBarHeight ?? layout.safeInsets.top, 1.0) @@ -81,13 +107,60 @@ final class NavigationSplitContainer: ASDisplayNode { ) } + private func masterWidth(for layout: ContainerViewLayout) -> CGFloat { + if case .tablet = layout.deviceMetrics.type, layout.size.width <= Metrics.avatarRailMaximumContainerWidth, !self.forceRegularMasterWidth { + return Metrics.compactMasterWidth + } + return min(max(Metrics.regularMasterMinWidth, floor(layout.size.width / 3.0)), floor(layout.size.width / 2.0)) + } + + @objc private func handleResizeTap(_ recognizer: UITapGestureRecognizer) { + if recognizer.state == .ended { + self.setForceRegularMasterWidth(!self.forceRegularMasterWidth) + } + } + + @objc private func handleResizePan(_ recognizer: UIPanGestureRecognizer) { + guard recognizer.state == .ended || recognizer.state == .cancelled else { + return + } + let translation = recognizer.translation(in: self.view) + if translation.x > 24.0 { + self.setForceRegularMasterWidth(true) + } else if translation.x < -24.0 { + self.setForceRegularMasterWidth(false) + } + } + + private func setForceRegularMasterWidth(_ value: Bool) { + if self.forceRegularMasterWidth == value { + return + } + self.forceRegularMasterWidth = value + UserDefaults.standard.set(value, forKey: "NavigationSplitContainer.forceRegularMasterWidth") + + if let layout = self.currentLayout { + self.update( + layout: layout, + masterControllers: self.masterControllers, + detailControllers: self.detailControllers, + detailsPlaceholderNode: self.currentDetailsPlaceholderNode, + transition: .animated(duration: 0.28, curve: .easeInOut) + ) + } + } + func update(layout: ContainerViewLayout, masterControllers: [ViewController], detailControllers: [ViewController], detailsPlaceholderNode: NavigationDetailsPlaceholderNode?, transition: ContainedViewLayoutTransition) { - let masterWidth: CGFloat = min(max(320.0, floor(layout.size.width / 3.0)), floor(layout.size.width / 2.0)) + self.currentLayout = layout + self.currentDetailsPlaceholderNode = detailsPlaceholderNode + + let masterWidth = self.masterWidth(for: layout) let detailWidth = layout.size.width - masterWidth transition.updateFrame(node: self.masterContainer, frame: CGRect(origin: CGPoint(), size: CGSize(width: masterWidth, height: layout.size.height))) transition.updateFrame(node: self.detailContainer, frame: CGRect(origin: CGPoint(x: masterWidth, y: 0.0), size: CGSize(width: detailWidth, height: layout.size.height))) transition.updateFrame(node: self.separator, frame: CGRect(origin: CGPoint(x: masterWidth, y: 0.0), size: CGSize(width: UIScreenPixel, height: layout.size.height))) + transition.updateFrame(node: self.resizeHandle, frame: CGRect(origin: CGPoint(x: masterWidth - 12.0, y: 0.0), size: CGSize(width: 24.0, height: layout.size.height))) if let detailsPlaceholderNode { let needsTiling = layout.size.width > layout.size.height diff --git a/submodules/TabBarUI/Sources/TabBarContollerNode.swift b/submodules/TabBarUI/Sources/TabBarContollerNode.swift index a44d64ee049..6c0facfe8fb 100644 --- a/submodules/TabBarUI/Sources/TabBarContollerNode.swift +++ b/submodules/TabBarUI/Sources/TabBarContollerNode.swift @@ -227,6 +227,7 @@ final class TabBarControllerNode: ASDisplayNode { if self.tabBarView.view == nil { tabBarTransition = .immediate } + let isCompactMasterController = params.layout.deviceMetrics.type == .tablet && params.layout.size.width <= 1133.0 && !UserDefaults.standard.bool(forKey: "NavigationSplitContainer.forceRegularMasterWidth") let tabBarSize = self.tabBarView.update( transition: tabBarTransition, component: AnyComponent(TabBarComponent( @@ -265,7 +266,7 @@ final class TabBarControllerNode: ASDisplayNode { } ) }, - search: self.currentController?.tabBarSearchState.flatMap { tabBarSearchState in + search: isCompactMasterController ? nil : self.currentController?.tabBarSearchState.flatMap { tabBarSearchState in return TabBarComponent.Search( isActive: tabBarSearchState.isActive, activate: { [weak self] in @@ -295,7 +296,8 @@ final class TabBarControllerNode: ASDisplayNode { self.view.addSubview(tabBarComponentView) } transition.updateFrame(view: tabBarComponentView, frame: tabBarFrame) - transition.updateAlpha(layer: tabBarComponentView.layer, alpha: params.toolbar == nil ? 1.0 : 0.0) + transition.updateAlpha(layer: tabBarComponentView.layer, alpha: (params.toolbar == nil && !isCompactMasterController) ? 1.0 : 0.0) + tabBarComponentView.isUserInteractionEnabled = !isCompactMasterController } transition.updateFrame(node: self.disabledOverlayNode, frame: tabBarFrame) From 592063fc849288a91f24716580dc3c2b5558df4e Mon Sep 17 00:00:00 2001 From: Aleksei Savin Date: Sun, 14 Jun 2026 01:40:44 +0300 Subject: [PATCH 02/12] fix: keep My Story label visible in compact rail --- .../Sources/StoryPeerListItemComponent.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift index 99d913e0319..5dd42b8d1df 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift @@ -1068,7 +1068,8 @@ public final class StoryPeerListItemComponent: Component { titleTransition.setPosition(view: self.titleContainer, position: titleContainerFrame.center) self.titleContainer.bounds = CGRect(origin: CGPoint(), size: titleContainerFrame.size) titleTransition.setScale(view: self.titleContainer, scale: effectiveScale) - titleTransition.setAlpha(view: self.titleContainer, alpha: component.expandedAlphaFraction) + let isOwnEmptyStoryItem = component.peer.id == component.context.account.peerId && !component.hasItems && component.ringAnimation == nil + titleTransition.setAlpha(view: self.titleContainer, alpha: isOwnEmptyStoryItem ? 1.0 : component.expandedAlphaFraction) if let ringAnimation = component.ringAnimation { var progressTransition = transition From 55ca379a44c9b453e71b8af98190ccb262d1eea6 Mon Sep 17 00:00:00 2001 From: Aleksei Savin Date: Sun, 14 Jun 2026 01:48:50 +0300 Subject: [PATCH 03/12] fix: pin My Story label layout in compact rail --- .../Sources/StoryPeerListItemComponent.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift index 5dd42b8d1df..6e693801a60 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift @@ -1063,12 +1063,14 @@ public final class StoryPeerListItemComponent: Component { } let titleContainerSize = CGSize(width: totalTitleWidth, height: titleSize.height) - let titleContainerFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleContainerSize.width) * 0.5) + (effectiveWidth - availableSize.width) * 0.5, y: indicatorFrame.midY + (indicatorFrame.height * 0.5 + 2.0) * effectiveScale), size: titleContainerSize) + let isOwnEmptyStoryItem = component.peer.id == component.context.account.peerId && !component.hasItems && component.ringAnimation == nil + let titleScale: CGFloat = isOwnEmptyStoryItem ? 1.0 : effectiveScale + let titleOffsetX: CGFloat = isOwnEmptyStoryItem ? 0.0 : (effectiveWidth - availableSize.width) * 0.5 + let titleContainerFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleContainerSize.width) * 0.5) + titleOffsetX, y: indicatorFrame.midY + (indicatorFrame.height * 0.5 + 2.0) * titleScale), size: titleContainerSize) titleTransition.setPosition(view: self.titleContainer, position: titleContainerFrame.center) self.titleContainer.bounds = CGRect(origin: CGPoint(), size: titleContainerFrame.size) - titleTransition.setScale(view: self.titleContainer, scale: effectiveScale) - let isOwnEmptyStoryItem = component.peer.id == component.context.account.peerId && !component.hasItems && component.ringAnimation == nil + titleTransition.setScale(view: self.titleContainer, scale: titleScale) titleTransition.setAlpha(view: self.titleContainer, alpha: isOwnEmptyStoryItem ? 1.0 : component.expandedAlphaFraction) if let ringAnimation = component.ringAnimation { From ce2d5cf0d5d1b29bc62273b6d840b03970b8205e Mon Sep 17 00:00:00 2001 From: Aleksei Savin Date: Sun, 14 Jun 2026 01:49:15 +0300 Subject: [PATCH 04/12] fix: keep own story item visible while compacted --- .../Sources/StoryPeerListComponent.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index 0b19bc56fcd..93e6daefaa1 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -1093,6 +1093,7 @@ public final class StoryPeerListComponent: Component { } else if peer.id == self.loadingItemId { itemRingAnimation = .loading } + let isOwnEmptyStoryItem = peer.id == component.context.account.peerId && !hasItems && itemRingAnimation == nil let measuredItem = calculateItem(i) @@ -1126,6 +1127,9 @@ public final class StoryPeerListComponent: Component { itemAlpha = collapsedState.sideAlphaFraction } } + if isOwnEmptyStoryItem { + itemAlpha = 1.0 + } var leftNeighborDistance: CGPoint? var rightNeighborDistance: CGPoint? @@ -1251,6 +1255,7 @@ public final class StoryPeerListComponent: Component { } else if let uploadProgress = component.uploadProgress[peer.id] { itemRingAnimation = .progress(uploadProgress) } + let isOwnEmptyStoryItem = peer.id == component.context.account.peerId && !hasItems && itemRingAnimation == nil let collapseIndex = i + effectiveFirstVisibleIndex let measuredItem = calculateItem(collapseIndex) @@ -1276,6 +1281,9 @@ public final class StoryPeerListComponent: Component { } else { itemAlpha = collapsedState.sideAlphaFraction } + if isOwnEmptyStoryItem { + itemAlpha = 1.0 + } var leftNeighborDistance: CGPoint? var rightNeighborDistance: CGPoint? From 47bf0ba1b97ffc129c2f5bb6020cf52133e082e2 Mon Sep 17 00:00:00 2001 From: Aleksei Savin Date: Sun, 14 Jun 2026 01:54:26 +0300 Subject: [PATCH 05/12] fix: hide empty story composer in compact rail --- .../Sources/StoryPeerListComponent.swift | 11 ++--------- .../Sources/StoryPeerListItemComponent.swift | 9 +++------ 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index 93e6daefaa1..47b82777ae2 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -1093,7 +1093,6 @@ public final class StoryPeerListComponent: Component { } else if peer.id == self.loadingItemId { itemRingAnimation = .loading } - let isOwnEmptyStoryItem = peer.id == component.context.account.peerId && !hasItems && itemRingAnimation == nil let measuredItem = calculateItem(i) @@ -1127,9 +1126,6 @@ public final class StoryPeerListComponent: Component { itemAlpha = collapsedState.sideAlphaFraction } } - if isOwnEmptyStoryItem { - itemAlpha = 1.0 - } var leftNeighborDistance: CGPoint? var rightNeighborDistance: CGPoint? @@ -1255,7 +1251,6 @@ public final class StoryPeerListComponent: Component { } else if let uploadProgress = component.uploadProgress[peer.id] { itemRingAnimation = .progress(uploadProgress) } - let isOwnEmptyStoryItem = peer.id == component.context.account.peerId && !hasItems && itemRingAnimation == nil let collapseIndex = i + effectiveFirstVisibleIndex let measuredItem = calculateItem(collapseIndex) @@ -1281,9 +1276,6 @@ public final class StoryPeerListComponent: Component { } else { itemAlpha = collapsedState.sideAlphaFraction } - if isOwnEmptyStoryItem { - itemAlpha = 1.0 - } var leftNeighborDistance: CGPoint? var rightNeighborDistance: CGPoint? @@ -1731,7 +1723,8 @@ public final class StoryPeerListComponent: Component { self.sortedItems.removeAll(keepingCapacity: true) if let storySubscriptions = component.storySubscriptions { - if !component.useHiddenList, let accountItem = storySubscriptions.accountItem { + let isCompactAvatarRail = availableSize.width <= 120.0 + if !component.useHiddenList, let accountItem = storySubscriptions.accountItem, !isCompactAvatarRail || accountItem.storyCount != 0 || accountItem.hasPending { self.sortedItems.append(accountItem) } diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift index 6e693801a60..99d913e0319 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift @@ -1063,15 +1063,12 @@ public final class StoryPeerListItemComponent: Component { } let titleContainerSize = CGSize(width: totalTitleWidth, height: titleSize.height) - let isOwnEmptyStoryItem = component.peer.id == component.context.account.peerId && !component.hasItems && component.ringAnimation == nil - let titleScale: CGFloat = isOwnEmptyStoryItem ? 1.0 : effectiveScale - let titleOffsetX: CGFloat = isOwnEmptyStoryItem ? 0.0 : (effectiveWidth - availableSize.width) * 0.5 - let titleContainerFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleContainerSize.width) * 0.5) + titleOffsetX, y: indicatorFrame.midY + (indicatorFrame.height * 0.5 + 2.0) * titleScale), size: titleContainerSize) + let titleContainerFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleContainerSize.width) * 0.5) + (effectiveWidth - availableSize.width) * 0.5, y: indicatorFrame.midY + (indicatorFrame.height * 0.5 + 2.0) * effectiveScale), size: titleContainerSize) titleTransition.setPosition(view: self.titleContainer, position: titleContainerFrame.center) self.titleContainer.bounds = CGRect(origin: CGPoint(), size: titleContainerFrame.size) - titleTransition.setScale(view: self.titleContainer, scale: titleScale) - titleTransition.setAlpha(view: self.titleContainer, alpha: isOwnEmptyStoryItem ? 1.0 : component.expandedAlphaFraction) + titleTransition.setScale(view: self.titleContainer, scale: effectiveScale) + titleTransition.setAlpha(view: self.titleContainer, alpha: component.expandedAlphaFraction) if let ringAnimation = component.ringAnimation { var progressTransition = transition From c0c747365a447525beb9eddc0df72d963100daac Mon Sep 17 00:00:00 2001 From: Aleksei Savin Date: Sun, 14 Jun 2026 02:01:31 +0300 Subject: [PATCH 06/12] fix: preserve folder tabs in compact rail --- submodules/ChatListUI/Sources/ChatListControllerNode.swift | 6 ++++-- .../Sources/StoryPeerListComponent.swift | 3 +-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index c31bcf8a325..6656de48ba6 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1501,7 +1501,8 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { var navigationHeaderPanels: AnyComponent? if self.controller?.tabContainerData != nil || !panels.isEmpty { var tabs: AnyComponent? - let isDesktopLikeCompactSidebar = layout.deviceMetrics.type == .tablet && layout.size.width <= 160.0 + let hasFolderTabs = (self.controller?.tabContainerData?.0.count ?? 0) > 1 + let isDesktopLikeCompactSidebar = layout.deviceMetrics.type == .tablet && layout.size.width <= 160.0 && !hasFolderTabs if let tabContainerData = self.controller?.tabContainerData, tabContainerData.0.count > 1, !isDesktopLikeCompactSidebar { let folderFilterIndex: (ChatListFilterTabEntryId, [ChatListFilterTabEntry]) -> Int? = { id, entries in var index = 0 @@ -1656,7 +1657,8 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { } } - let isDesktopLikeCompactSidebar = layout.deviceMetrics.type == .tablet && layout.size.width <= 160.0 + let hasFolderTabs = (self.controller?.tabContainerData?.0.count ?? 0) > 1 + let isDesktopLikeCompactSidebar = layout.deviceMetrics.type == .tablet && layout.size.width <= 160.0 && !hasFolderTabs if isDesktopLikeCompactSidebar { if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { navigationBarComponentView.isHidden = true diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index 47b82777ae2..0b19bc56fcd 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -1723,8 +1723,7 @@ public final class StoryPeerListComponent: Component { self.sortedItems.removeAll(keepingCapacity: true) if let storySubscriptions = component.storySubscriptions { - let isCompactAvatarRail = availableSize.width <= 120.0 - if !component.useHiddenList, let accountItem = storySubscriptions.accountItem, !isCompactAvatarRail || accountItem.storyCount != 0 || accountItem.hasPending { + if !component.useHiddenList, let accountItem = storySubscriptions.accountItem { self.sortedItems.append(accountItem) } From c8339d56445d7ce1d0e2decafe8592c579ddf076 Mon Sep 17 00:00:00 2001 From: Aleksei Savin Date: Sun, 14 Jun 2026 02:04:47 +0300 Subject: [PATCH 07/12] fix: hide search in compact folder rail --- submodules/ChatListUI/Sources/ChatListControllerNode.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 6656de48ba6..cf0d4d11274 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1658,7 +1658,8 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { } let hasFolderTabs = (self.controller?.tabContainerData?.0.count ?? 0) > 1 - let isDesktopLikeCompactSidebar = layout.deviceMetrics.type == .tablet && layout.size.width <= 160.0 && !hasFolderTabs + let isCompactAvatarRail = layout.deviceMetrics.type == .tablet && layout.size.width <= 160.0 + let isDesktopLikeCompactSidebar = isCompactAvatarRail && !hasFolderTabs if isDesktopLikeCompactSidebar { if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { navigationBarComponentView.isHidden = true @@ -1675,7 +1676,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { strings: self.presentationData.strings, statusBarHeight: layout.statusBarHeight ?? 0.0, sideInset: layout.safeInsets.left, - search: ChatListNavigationBar.Search(isEnabled: true), + search: ChatListNavigationBar.Search(isEnabled: !isCompactAvatarRail), activeSearch: self.isSearchDisplayControllerActive, primaryContent: headerContent?.primaryContent, secondaryContent: headerContent?.secondaryContent, From e7e40d3a9090a90e800552ddecee53e29806e665 Mon Sep 17 00:00:00 2001 From: Aleksei Savin Date: Sun, 14 Jun 2026 02:13:19 +0300 Subject: [PATCH 08/12] fix: keep compact rail clean with my story --- .../ChatListUI/Sources/ChatListControllerNode.swift | 9 +++------ .../Sources/StoryPeerListComponent.swift | 3 ++- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index cf0d4d11274..c31bcf8a325 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1501,8 +1501,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { var navigationHeaderPanels: AnyComponent? if self.controller?.tabContainerData != nil || !panels.isEmpty { var tabs: AnyComponent? - let hasFolderTabs = (self.controller?.tabContainerData?.0.count ?? 0) > 1 - let isDesktopLikeCompactSidebar = layout.deviceMetrics.type == .tablet && layout.size.width <= 160.0 && !hasFolderTabs + let isDesktopLikeCompactSidebar = layout.deviceMetrics.type == .tablet && layout.size.width <= 160.0 if let tabContainerData = self.controller?.tabContainerData, tabContainerData.0.count > 1, !isDesktopLikeCompactSidebar { let folderFilterIndex: (ChatListFilterTabEntryId, [ChatListFilterTabEntry]) -> Int? = { id, entries in var index = 0 @@ -1657,9 +1656,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { } } - let hasFolderTabs = (self.controller?.tabContainerData?.0.count ?? 0) > 1 - let isCompactAvatarRail = layout.deviceMetrics.type == .tablet && layout.size.width <= 160.0 - let isDesktopLikeCompactSidebar = isCompactAvatarRail && !hasFolderTabs + let isDesktopLikeCompactSidebar = layout.deviceMetrics.type == .tablet && layout.size.width <= 160.0 if isDesktopLikeCompactSidebar { if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { navigationBarComponentView.isHidden = true @@ -1676,7 +1673,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { strings: self.presentationData.strings, statusBarHeight: layout.statusBarHeight ?? 0.0, sideInset: layout.safeInsets.left, - search: ChatListNavigationBar.Search(isEnabled: !isCompactAvatarRail), + search: ChatListNavigationBar.Search(isEnabled: true), activeSearch: self.isSearchDisplayControllerActive, primaryContent: headerContent?.primaryContent, secondaryContent: headerContent?.secondaryContent, diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index 0b19bc56fcd..5e5238bfa7a 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -1145,7 +1145,8 @@ public final class StoryPeerListComponent: Component { unseenCount = itemSet.unseenCount var composeContentOffset: CGFloat? - if peer.id == component.context.account.peerId && collapsedState.sideAlphaFraction == 1.0 && self.scrollView.contentOffset.x < 0.0 { + let isCompactAvatarRail = itemLayout.containerSize.width <= 120.0 + if !isCompactAvatarRail && peer.id == component.context.account.peerId && collapsedState.sideAlphaFraction == 1.0 && self.scrollView.contentOffset.x < 0.0 { composeContentOffset = self.scrollView.contentOffset.x * -1.0 } From 31d69c44e920768ba0fbb36f8e3accb99941990e Mon Sep 17 00:00:00 2001 From: Aleksei Savin Date: Sun, 14 Jun 2026 02:18:47 +0300 Subject: [PATCH 09/12] fix: remove own story hit target from compact rail --- .../Sources/StoryPeerListComponent.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index 5e5238bfa7a..6394b08f2b7 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -1724,7 +1724,8 @@ public final class StoryPeerListComponent: Component { self.sortedItems.removeAll(keepingCapacity: true) if let storySubscriptions = component.storySubscriptions { - if !component.useHiddenList, let accountItem = storySubscriptions.accountItem { + let isCompactAvatarRail = availableSize.width <= 120.0 + if !component.useHiddenList, !isCompactAvatarRail, let accountItem = storySubscriptions.accountItem { self.sortedItems.append(accountItem) } From f40d329154d4ee8e874bd18ce6549e82ebbf841c Mon Sep 17 00:00:00 2001 From: Aleksei Savin Date: Sun, 14 Jun 2026 02:22:33 +0300 Subject: [PATCH 10/12] fix: hide stories in compact avatar rail --- submodules/ChatListUI/Sources/ChatListControllerNode.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index c31bcf8a325..396550b499e 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1645,8 +1645,12 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { )) } + let isDesktopLikeCompactSidebar = layout.deviceMetrics.type == .tablet && layout.size.width <= 160.0 + var effectiveStorySubscriptions: EngineStorySubscriptions? - if let controller = self.controller, case .forum = controller.location { + if isDesktopLikeCompactSidebar { + effectiveStorySubscriptions = EngineStorySubscriptions(accountItem: nil, items: [], hasMoreToken: nil) + } else if let controller = self.controller, case .forum = controller.location { effectiveStorySubscriptions = nil } else { if let controller = self.controller, let storySubscriptions = controller.orderedStorySubscriptions, shouldDisplayStoriesInChatListHeader(storySubscriptions: storySubscriptions, isHidden: controller.location == .chatList(groupId: .archive)) { @@ -1656,7 +1660,6 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { } } - let isDesktopLikeCompactSidebar = layout.deviceMetrics.type == .tablet && layout.size.width <= 160.0 if isDesktopLikeCompactSidebar { if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { navigationBarComponentView.isHidden = true From 97cd6a8162e12060a00eb7b8790ca6db965a9b59 Mon Sep 17 00:00:00 2001 From: Aleksei Savin Date: Sun, 14 Jun 2026 02:28:00 +0300 Subject: [PATCH 11/12] fix: disable story gestures in compact rail --- .../Sources/ChatListControllerNode.swift | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 396550b499e..958e5bfbf3e 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -249,7 +249,8 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele return } - if !self.isInlineMode, itemNode.listNode.isTracking && !self.currentItemNode.startedScrollingAtUpperBound && self.tempTopInset == 0.0 { + let isCompactAvatarRail = self.validLayout?.layout.deviceMetrics.type == .tablet && (self.validLayout?.layout.size.width ?? .greatestFiniteMagnitude) <= 160.0 + if !isCompactAvatarRail && !self.isInlineMode, itemNode.listNode.isTracking && !self.currentItemNode.startedScrollingAtUpperBound && self.tempTopInset == 0.0 { if case let .known(value) = offset { if value < -1.0 { if let controller = self.controller, let storySubscriptions = controller.orderedStorySubscriptions, shouldDisplayStoriesInChatListHeader(storySubscriptions: storySubscriptions, isHidden: controller.location == .chatList(groupId: .archive)) { @@ -293,7 +294,10 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele } let tempTopInset: CGFloat - if validLayout.inlineNavigationLocation != nil { + let isCompactAvatarRail = validLayout.layout.deviceMetrics.type == .tablet && validLayout.layout.size.width <= 160.0 + if isCompactAvatarRail { + tempTopInset = 0.0 + } else if validLayout.inlineNavigationLocation != nil { tempTopInset = 0.0 } else if self.currentItemNode.startedScrollingAtUpperBound && !self.isInlineMode { if let controller = self.controller, let storySubscriptions = controller.orderedStorySubscriptions, shouldDisplayStoriesInChatListHeader(storySubscriptions: storySubscriptions, isHidden: controller.location == .chatList(groupId: .archive)) { @@ -534,7 +538,12 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele self.applyItemNodeAsCurrent(id: .all, itemNode: itemNode) let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in - guard let self, self.availableFilters.count > 1 || (self.controller?.isStoryPostingAvailable == true && !(self.context.sharedContext.callManager?.hasActiveCall ?? false)) else { + guard let self else { + return [] + } + let isCompactAvatarRail = self.validLayout?.layout.deviceMetrics.type == .tablet && (self.validLayout?.layout.size.width ?? .greatestFiniteMagnitude) <= 160.0 + let isStoryPostingAvailable = !isCompactAvatarRail && (self.controller?.isStoryPostingAvailable == true && !(self.context.sharedContext.callManager?.hasActiveCall ?? false)) + guard self.availableFilters.count > 1 || isStoryPostingAvailable else { return [] } guard case .chatList(.root) = self.location else { @@ -639,7 +648,8 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele hasLiveStream = true } - if case .compact = layout.metrics.widthClass, self.controller?.isStoryPostingAvailable == true && !(self.context.sharedContext.callManager?.hasActiveCall ?? false) { + let isCompactAvatarRail = layout.deviceMetrics.type == .tablet && layout.size.width <= 160.0 + if !isCompactAvatarRail, case .compact = layout.metrics.widthClass, self.controller?.isStoryPostingAvailable == true && !(self.context.sharedContext.callManager?.hasActiveCall ?? false) { if hasLiveStream { if translation.x >= 30.0 { self.panRecognizer?.cancel() From afe5370e428d028c52b72de9060e367be1a264e1 Mon Sep 17 00:00:00 2001 From: Aleksei Savin Date: Sun, 14 Jun 2026 19:26:01 +0300 Subject: [PATCH 12/12] fix: make compact rail top spacer noninteractive --- submodules/ChatListUI/Sources/ChatListContainerItemNode.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/submodules/ChatListUI/Sources/ChatListContainerItemNode.swift b/submodules/ChatListUI/Sources/ChatListContainerItemNode.swift index 0d77a965cac..f384fcf80e6 100644 --- a/submodules/ChatListUI/Sources/ChatListContainerItemNode.swift +++ b/submodules/ChatListUI/Sources/ChatListContainerItemNode.swift @@ -306,6 +306,10 @@ final class ChatListContainerItemNode: ASDisplayNode { var listInsets = insets var additionalTopInset: CGFloat = 0.0 + let isCompactAvatarRail = size.width <= 160.0 + if isCompactAvatarRail { + listInsets.top += 74.0 + } if let chatFolderUpdates = self.chatFolderUpdates { let topPanel: TopPanelItem