From ff0e21e4121e3a83e06f1ee97ecd7b1dee58a115 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Sat, 24 Jun 2023 13:43:57 +0300 Subject: [PATCH 1/4] Tooltip --- .../Sources/ChatListController.swift | 85 ++++++++++++++++--- .../Sources/ChatListControllerNode.swift | 4 +- .../TelegramNotices/Sources/Notices.swift | 25 ++++++ .../Sources/StoryPeerListComponent.swift | 4 + 4 files changed, 104 insertions(+), 14 deletions(-) diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 3134c99612..eea9e7d14e 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -181,7 +181,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController private var rawStorySubscriptions: EngineStorySubscriptions? private var shouldFixStorySubscriptionOrder: Bool = false private var fixedStorySubscriptionOrder: [EnginePeer.Id] = [] - var orderedStorySubscriptions: EngineStorySubscriptions? + private(set) var orderedStorySubscriptions: EngineStorySubscriptions? + private var displayedStoriesTooltip: Bool = false private var storyProgressDisposable: Disposable? private var storySubscriptionsDisposable: Disposable? @@ -1853,11 +1854,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } - var wasEmpty = true - if let rawStorySubscriptions = self.rawStorySubscriptions, !rawStorySubscriptions.items.isEmpty { - wasEmpty = false - } - self.rawStorySubscriptions = rawStorySubscriptions var items: [EngineStorySubscriptions.Item] = [] if self.shouldFixStorySubscriptionOrder { @@ -1879,8 +1875,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController ) self.fixedStorySubscriptionOrder = items.map(\.peer.id) - let isEmpty = rawStorySubscriptions.items.isEmpty - let transition: ContainedViewLayoutTransition if self.didAppear { transition = .animated(duration: 0.4, curve: .spring) @@ -1888,9 +1882,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController transition = .immediate } - let _ = wasEmpty - let _ = isEmpty - self.chatListDisplayNode.temporaryContentOffsetChangeTransition = transition self.requestLayout(transition: transition) self.chatListDisplayNode.temporaryContentOffsetChangeTransition = nil @@ -1911,6 +1902,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } self.storiesReady.set(.single(true)) + + Queue.mainQueue().after(1.0, { [weak self] in + guard let self else { + return + } + self.maybeDisplayStoryTooltip() + }) }) self.storyProgressDisposable = (self.context.engine.messages.allStoriesUploadProgress() |> deliverOnMainQueue).start(next: { [weak self] progress in @@ -1922,6 +1920,67 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } + fileprivate func maybeDisplayStoryTooltip() { + let content = self.updateHeaderContent() + if content.secondaryContent != nil { + return + } + guard let chatListTitle = content.primaryContent?.chatListTitle else { + return + } + if chatListTitle.activity { + return + } + if self.displayedStoriesTooltip { + return + } + + if let orderedStorySubscriptions = self.orderedStorySubscriptions, !orderedStorySubscriptions.items.isEmpty { + let _ = (ApplicationSpecificNotice.displayChatListStoriesTooltip(accountManager: self.context.sharedContext.accountManager) + |> deliverOnMainQueue).start(next: { [weak self] didDisplay in + guard let self else { + return + } + if didDisplay { + return + } + + if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View, !navigationBarView.storiesUnlocked, !self.displayedStoriesTooltip { + if let storyPeerListView = self.chatListHeaderView()?.storyPeerListView(), let (anchorView, anchorRect) = storyPeerListView.anchorForTooltip() { + self.displayedStoriesTooltip = true + + let absoluteFrame = anchorView.convert(anchorRect, to: self.view) + //TODO:localize + + let itemList = orderedStorySubscriptions.items.prefix(3).map(\.peer.compactDisplayTitle) + var itemListString: String = itemList.joined(separator: ", ") + if #available(iOS 13.0, *) { + let listFormatter = ListFormatter() + listFormatter.locale = localeWithStrings(self.presentationData.strings) + if let value = listFormatter.string(from: itemList) { + itemListString = value + } + } + + let text: String = "Tap above to view updates\nfrom \(itemListString)" + + let tooltipController = TooltipController(content: .text(text), baseFontSize: self.presentationData.listsFontSize.baseDisplaySize, timeout: 30.0, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true, padding: 6.0, innerPadding: UIEdgeInsets(top: 2.0, left: 3.0, bottom: 2.0, right: 3.0)) + self.present(tooltipController, in: .current, with: TooltipControllerPresentationArguments(sourceNodeAndRect: { [weak self] in + guard let self else { + return nil + } + return (self.displayNode, absoluteFrame.insetBy(dx: 0.0, dy: 0.0).offsetBy(dx: 0.0, dy: 0.0)) + })) + + #if !DEBUG + let _ = ApplicationSpecificNotice.setDisplayChatListStoriesTooltip(accountManager: self.context.sharedContext.accountManager).start() + #endif + } + } + }) + } + } + public override func displayNodeDidLoad() { super.displayNodeDidLoad() @@ -2351,7 +2410,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } - func updateHeaderContent(layout: ContainerViewLayout) -> (primaryContent: ChatListHeaderComponent.Content?, secondaryContent: ChatListHeaderComponent.Content?) { + func updateHeaderContent() -> (primaryContent: ChatListHeaderComponent.Content?, secondaryContent: ChatListHeaderComponent.Content?) { var primaryContent: ChatListHeaderComponent.Content? if let primaryContext = self.primaryContext { var backTitle: String? @@ -5684,6 +5743,8 @@ private final class ChatListLocationContext { } self.parentController?.requestLayout(transition: .animated(duration: 0.45, curve: .spring)) + + self.parentController?.maybeDisplayStoryTooltip() } private func updateForum( diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 3e3d9c0291..3b71c025ae 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1915,7 +1915,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { } private func updateNavigationBar(layout: ContainerViewLayout, deferScrollApplication: Bool, transition: Transition) -> (navigationHeight: CGFloat, storiesInset: CGFloat) { - let headerContent = self.controller?.updateHeaderContent(layout: layout) + let headerContent = self.controller?.updateHeaderContent() var tabsNode: ASDisplayNode? var tabsNodeIsSearch = false @@ -2355,7 +2355,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { } private func shouldStopScrolling(listView: ListView, velocity: CGFloat, isPrimary: Bool) -> Bool { - if abs(velocity) > 1.0 { + if abs(velocity) > 0.8 { return false } diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index aef9b83c44..b7fd91b2fa 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -173,6 +173,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case chatWallpaperLightPreviewTip = 39 case chatWallpaperDarkPreviewTip = 40 case displayChatListContacts = 41 + case displayChatListStoriesTooltip = 42 var key: ValueBoxKey { let v = ValueBoxKey(length: 4) @@ -394,6 +395,10 @@ private struct ApplicationSpecificNoticeKeys { static func displayChatListContacts() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.displayChatListContacts.key) } + + static func displayChatListStoriesTooltip() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.displayChatListStoriesTooltip.key) + } } public struct ApplicationSpecificNotice { @@ -1445,6 +1450,26 @@ public struct ApplicationSpecificNotice { |> ignoreValues } + public static func displayChatListStoriesTooltip(accountManager: AccountManager) -> Signal { + return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.displayChatListStoriesTooltip()) + |> map { view -> Bool in + if let _ = view.value?.get(ApplicationSpecificBoolNotice.self) { + return true + } else { + return false + } + } + } + + public static func setDisplayChatListStoriesTooltip(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Void in + if let entry = CodableEntry(ApplicationSpecificBoolNotice()) { + transaction.setNotice(ApplicationSpecificNoticeKeys.displayChatListStoriesTooltip(), entry) + } + } + |> ignoreValues + } + public static func reset(accountManager: AccountManager) -> Signal { return accountManager.transaction { transaction -> Void in } diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index 59e34ca326..45c84e0515 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -323,6 +323,10 @@ public final class StoryPeerListComponent: Component { }) } + public func anchorForTooltip() -> (UIView, CGRect)? { + return (self.collapsedButton, self.collapsedButton.bounds) + } + public func transitionViewForItem(peerId: EnginePeer.Id) -> (UIView, StoryContainerScreen.TransitionView)? { if self.collapsedButton.isUserInteractionEnabled { return nil From 3a22e32fb15ee2c48226fa00133a5b3405570c9d Mon Sep 17 00:00:00 2001 From: Ali <> Date: Sat, 24 Jun 2023 13:45:19 +0300 Subject: [PATCH 2/4] Roll back debugging --- Telegram/BUILD | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Telegram/BUILD b/Telegram/BUILD index b6347a6a0f..00a75be7dd 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -1978,12 +1978,12 @@ ios_application( extensions = select({ ":disableExtensionsSetting": [], "//conditions:default": [ - #":ShareExtension", - #":NotificationContentExtension", + ":ShareExtension", + ":NotificationContentExtension", ":NotificationServiceExtension", - #":IntentsExtension", - #":WidgetExtension", - #":BroadcastUploadExtension", + ":IntentsExtension", + ":WidgetExtension", + ":BroadcastUploadExtension", ], }), watch_application = select({ @@ -2013,9 +2013,9 @@ xcodeproj( "Debug": { "//command_line_option:compilation_mode": "dbg", }, - #"Release": { - # "//command_line_option:compilation_mode": "opt", - #}, + "Release": { + "//command_line_option:compilation_mode": "opt", + }, }, default_xcode_configuration = "Debug" From 94e6f28efe43c25d78b8b0fd81a8d33236c70512 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Sat, 24 Jun 2023 14:06:04 +0300 Subject: [PATCH 3/4] Adjust tap --- .../Sources/ChatListController.swift | 7 ++++- .../Sources/ContactsControllerNode.swift | 2 +- .../Sources/ChatListNavigationBar.swift | 29 ++----------------- 3 files changed, 9 insertions(+), 29 deletions(-) diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index eea9e7d14e..a4b4776d43 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -5744,7 +5744,12 @@ private final class ChatListLocationContext { self.parentController?.requestLayout(transition: .animated(duration: 0.45, curve: .spring)) - self.parentController?.maybeDisplayStoryTooltip() + Queue.mainQueue().after(1.0, { [weak self] in + guard let self else { + return + } + self.parentController?.maybeDisplayStoryTooltip() + }) } private func updateForum( diff --git a/submodules/ContactListUI/Sources/ContactsControllerNode.swift b/submodules/ContactListUI/Sources/ContactsControllerNode.swift index fd1819c371..078e3302ae 100644 --- a/submodules/ContactListUI/Sources/ContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsControllerNode.swift @@ -490,7 +490,7 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { } if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { - navigationBarComponentView.applyScroll(offset: offset, allowAvatarsExpansion: true, transition: Transition(transition)) + navigationBarComponentView.applyScroll(offset: offset, allowAvatarsExpansion: false, transition: Transition(transition)) } } diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift index d5d24278f7..6c2d8421f7 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift @@ -298,28 +298,6 @@ public final class ChatListNavigationBar: Component { self.addSubview(searchContentNode.view) } - /*let clippedStoriesOverscrollOffset = -min(0.0, clippedScrollOffset) - let clippedStoriesOffset = max(0.0, min(clippedScrollOffset, defaultStoriesOffsetDistance)) - var storiesOffsetFraction: CGFloat - var storiesUnlockedOffsetFraction: CGFloat - if !component.isSearchActive, component.secondaryTransition == 0.0, let storySubscriptions = component.storySubscriptions, !storySubscriptions.items.isEmpty, allowAvatarsExpansion { - if component.storiesUnlocked { - storiesOffsetFraction = clippedStoriesOffset / defaultStoriesOffsetDistance - storiesUnlockedOffsetFraction = 1.0 - } else { - storiesOffsetFraction = 1.0 - (clippedStoriesOverscrollOffset / defaultStoriesOffsetDistance) - storiesUnlockedOffsetFraction = 1.0 - } - } else { - storiesOffsetFraction = 1.0 - storiesUnlockedOffsetFraction = 1.0 - } - - if self.applyScrollFractionAnimator != nil { - storiesOffsetFraction = self.applyScrollFraction * storiesOffsetFraction + (1.0 - self.applyScrollFraction) * self.storiesOffsetStartFraction - storiesUnlockedOffsetFraction = self.applyScrollUnlockedFraction * storiesUnlockedOffsetFraction + (1.0 - self.applyScrollUnlockedFraction) * self.storiesUnlockedStartFraction - }*/ - let searchSize = CGSize(width: currentLayout.size.width, height: navigationBarSearchContentHeight) var searchFrame = CGRect(origin: CGPoint(x: 0.0, y: visibleSize.height - searchSize.height), size: searchSize) if component.tabsNode != nil { @@ -338,9 +316,6 @@ public final class ChatListNavigationBar: Component { searchContentNode.updateLayout(size: searchSize, leftInset: component.sideInset, rightInset: component.sideInset, transition: transition.containedViewLayoutTransition) let headerTransition = transition - /*if self.applyScrollFractionAnimator != nil { - headerTransition = .immediate - }*/ let storiesOffsetFraction: CGFloat let storiesUnlocked: Bool @@ -362,9 +337,9 @@ public final class ChatListNavigationBar: Component { if allowAvatarsExpansion && transition.animation.isImmediate { if self.storiesUnlocked != storiesUnlocked { if storiesUnlocked { - HapticFeedback().impact() + HapticFeedback().impact(.veryLight) } else { - HapticFeedback().tap() + HapticFeedback().impact(.veryLight) } } } From a0817a831bb7641828e2c4733ef5e29a4d52d325 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Sat, 24 Jun 2023 15:25:11 +0300 Subject: [PATCH 4/4] Avatar story indicator counters --- .../Sources/Node/ChatListItem.swift | 3 +- .../Sources/ContactListNode.swift | 7 +- .../Sources/ContactsPeerItem.swift | 15 +-- .../Sources/ChatAvatarNavigationNode.swift | 3 +- .../AvatarStoryIndicatorComponent.swift | 105 +++++++++++++++--- .../Sources/PeerInfo/PeerInfoHeaderNode.swift | 3 +- 6 files changed, 105 insertions(+), 31 deletions(-) diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index ac741a0dc0..aa9e89271c 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -2795,7 +2795,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { hasUnseen: displayStoryIndicator, isDarkTheme: item.presentationData.theme.overallDarkAppearance, activeLineWidth: 2.0, - inactiveLineWidth: 1.0 + UIScreenPixel + inactiveLineWidth: 1.0 + UIScreenPixel, + counters: nil )), environment: {}, containerSize: indicatorFrame.size diff --git a/submodules/ContactListUI/Sources/ContactListNode.swift b/submodules/ContactListUI/Sources/ContactListNode.swift index 2e36d8e53f..4f71ae67e6 100644 --- a/submodules/ContactListUI/Sources/ContactListNode.swift +++ b/submodules/ContactListUI/Sources/ContactListNode.swift @@ -201,8 +201,10 @@ private enum ContactListNodeEntry: Comparable, Identifiable { })] } - var hasUnseenStories: Bool? + var storyStats: (total: Int, unseen: Int)? if let storyData = storyData { + storyStats = (storyData.count, storyData.unseenCount) + let text: String //TODO:localize if storyData.unseenCount != 0 { @@ -219,12 +221,11 @@ private enum ContactListNodeEntry: Comparable, Identifiable { } } status = .custom(string: text, multiline: false, isActive: false, icon: nil) - hasUnseenStories = storyData.unseenCount != 0 } return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: isSearch ? .generalSearch : .peer, peer: itemPeer, status: status, enabled: enabled, selection: selection, selectionPosition: .left, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), additionalActions: additionalActions, index: nil, header: header, action: { _ in interaction.openPeer(peer, .generic) - }, itemHighlighting: interaction.itemHighlighting, contextAction: itemContextAction, hasUnseenStories: hasUnseenStories, openStories: { peer, sourceNode in + }, itemHighlighting: interaction.itemHighlighting, contextAction: itemContextAction, storyStats: storyStats, openStories: { peer, sourceNode in if case let .peer(peerValue, _) = peer, let peerValue { interaction.openStories(peerValue, sourceNode) } diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index db79ce7b9c..64a66ee182 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -180,7 +180,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { let arrowAction: (() -> Void)? let animationCache: AnimationCache? let animationRenderer: MultiAnimationRenderer? - let hasUnseenStories: Bool? + let storyStats: (total: Int, unseen: Int)? let openStories: ((ContactsPeerItemPeer, ASDisplayNode) -> Void)? public let selectable: Bool @@ -217,7 +217,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? = nil, arrowAction: (() -> Void)? = nil, animationCache: AnimationCache? = nil, animationRenderer: MultiAnimationRenderer? = nil, - hasUnseenStories: Bool? = nil, + storyStats: (total: Int, unseen: Int)? = nil, openStories: ((ContactsPeerItemPeer, ASDisplayNode) -> Void)? = nil ) { self.presentationData = presentationData @@ -248,7 +248,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { self.arrowAction = arrowAction self.animationCache = animationCache self.animationRenderer = animationRenderer - self.hasUnseenStories = hasUnseenStories + self.storyStats = storyStats self.openStories = openStories if let index = index { @@ -1088,7 +1088,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { var avatarScale: CGFloat = 1.0 - if item.hasUnseenStories != nil { + if item.storyStats != nil { avatarScale *= (avatarFrame.width - 2.0 * 2.0) / avatarFrame.width } @@ -1096,7 +1096,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { let storyIndicatorScale: CGFloat = 1.0 - if let displayStoryIndicator = item.hasUnseenStories { + if let storyStats = item.storyStats { var indicatorTransition = Transition(transition) let avatarStoryIndicator: ComponentView if let current = strongSelf.avatarStoryIndicator { @@ -1113,10 +1113,11 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { let _ = avatarStoryIndicator.update( transition: indicatorTransition, component: AnyComponent(AvatarStoryIndicatorComponent( - hasUnseen: displayStoryIndicator, + hasUnseen: storyStats.unseen != 0, isDarkTheme: item.presentationData.theme.overallDarkAppearance, activeLineWidth: 1.0 + UIScreenPixel, - inactiveLineWidth: 1.0 + UIScreenPixel + inactiveLineWidth: 1.0 + UIScreenPixel, + counters: AvatarStoryIndicatorComponent.Counters(totalCount: storyStats.total, unseenCount: storyStats.unseen) )), environment: {}, containerSize: indicatorFrame.size diff --git a/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift b/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift index 17cfb99cb2..048295307d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift @@ -215,7 +215,8 @@ public final class ChatAvatarNavigationNode: ASDisplayNode { hasUnseen: hasUnseenStories, isDarkTheme: theme.overallDarkAppearance, activeLineWidth: 1.0, - inactiveLineWidth: 1.0 + inactiveLineWidth: 1.0, + counters: nil )), environment: {}, containerSize: self.avatarNode.bounds.insetBy(dx: 2.0, dy: 2.0).size diff --git a/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift b/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift index f857f63b65..fd5b09f3f4 100644 --- a/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift +++ b/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift @@ -5,21 +5,34 @@ import ComponentFlow import TelegramPresentationData public final class AvatarStoryIndicatorComponent: Component { + public struct Counters: Equatable { + public var totalCount: Int + public var unseenCount: Int + + public init(totalCount: Int, unseenCount: Int) { + self.totalCount = totalCount + self.unseenCount = unseenCount + } + } + public let hasUnseen: Bool public let isDarkTheme: Bool public let activeLineWidth: CGFloat public let inactiveLineWidth: CGFloat + public let counters: Counters? public init( hasUnseen: Bool, isDarkTheme: Bool, activeLineWidth: CGFloat, - inactiveLineWidth: CGFloat + inactiveLineWidth: CGFloat, + counters: Counters? ) { self.hasUnseen = hasUnseen self.isDarkTheme = isDarkTheme self.activeLineWidth = activeLineWidth self.inactiveLineWidth = inactiveLineWidth + self.counters = counters } public static func ==(lhs: AvatarStoryIndicatorComponent, rhs: AvatarStoryIndicatorComponent) -> Bool { @@ -35,6 +48,9 @@ public final class AvatarStoryIndicatorComponent: Component { if lhs.inactiveLineWidth != rhs.inactiveLineWidth { return false } + if lhs.counters != rhs.counters { + return false + } return true } @@ -76,26 +92,79 @@ public final class AvatarStoryIndicatorComponent: Component { context.clear(CGRect(origin: CGPoint(), size: size)) context.setLineWidth(lineWidth) - context.addEllipse(in: CGRect(origin: CGPoint(x: size.width * 0.5 - diameter * 0.5, y: size.height * 0.5 - diameter * 0.5), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5)) - context.replacePathWithStrokedPath() - context.clip() - var locations: [CGFloat] = [1.0, 0.0] - let colors: [CGColor] - if component.hasUnseen { - colors = [UIColor(rgb: 0x34C76F).cgColor, UIColor(rgb: 0x3DA1FD).cgColor] - } else { - if component.isDarkTheme { - colors = [UIColor(rgb: 0x48484A).cgColor, UIColor(rgb: 0x48484A).cgColor] - } else { - colors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor] + if let counters = component.counters, counters.totalCount > 1 { + let center = CGPoint(x: size.width * 0.5, y: size.height * 0.5) + let radius = (diameter - lineWidth) * 0.5 + let spacing: CGFloat = 2.0 + let angularSpacing: CGFloat = spacing / radius + let circleLength = CGFloat.pi * 2.0 * radius + let segmentLength = (circleLength - spacing * CGFloat(counters.totalCount)) / CGFloat(counters.totalCount) + let segmentAngle = segmentLength / radius + + for pass in 0 ..< 2 { + context.resetClip() + + let startIndex: Int + let endIndex: Int + if pass == 0 { + startIndex = 0 + endIndex = counters.totalCount - counters.unseenCount + } else { + startIndex = counters.totalCount - counters.unseenCount + endIndex = counters.totalCount + } + if startIndex < endIndex { + for i in startIndex ..< endIndex { + let startAngle = CGFloat(i) * (angularSpacing + segmentAngle) - CGFloat.pi * 0.5 + angularSpacing * 0.5 + context.move(to: CGPoint(x: center.x + cos(startAngle) * radius, y: center.y + sin(startAngle) * radius)) + context.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: startAngle + segmentAngle, clockwise: false) + } + + context.replacePathWithStrokedPath() + context.clip() + + var locations: [CGFloat] = [1.0, 0.0] + let colors: [CGColor] + if pass == 1 { + colors = [UIColor(rgb: 0x34C76F).cgColor, UIColor(rgb: 0x3DA1FD).cgColor] + } else { + if component.isDarkTheme { + colors = [UIColor(rgb: 0x48484A).cgColor, UIColor(rgb: 0x48484A).cgColor] + } else { + colors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor] + } + } + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + } } + } else { + context.addEllipse(in: CGRect(origin: CGPoint(x: size.width * 0.5 - diameter * 0.5, y: size.height * 0.5 - diameter * 0.5), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5)) + + context.replacePathWithStrokedPath() + context.clip() + + var locations: [CGFloat] = [1.0, 0.0] + let colors: [CGColor] + if component.hasUnseen { + colors = [UIColor(rgb: 0x34C76F).cgColor, UIColor(rgb: 0x3DA1FD).cgColor] + } else { + if component.isDarkTheme { + colors = [UIColor(rgb: 0x48484A).cgColor, UIColor(rgb: 0x48484A).cgColor] + } else { + colors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor] + } + } + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) } - - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! - - context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) }) transition.setFrame(view: self.indicatorView, frame: CGRect(origin: CGPoint(x: (availableSize.width - imageDiameter) * 0.5, y: (availableSize.height - imageDiameter) * 0.5), size: CGSize(width: imageDiameter, height: imageDiameter))) diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index 705c8e4fe1..d6891017ad 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -470,7 +470,8 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { hasUnseen: hasUnseenStories, isDarkTheme: theme.overallDarkAppearance, activeLineWidth: 3.0, - inactiveLineWidth: 2.0 + inactiveLineWidth: 2.0, + counters: nil )), environment: {}, containerSize: self.avatarNode.bounds.size