From a0817a831bb7641828e2c4733ef5e29a4d52d325 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Sat, 24 Jun 2023 15:25:11 +0300 Subject: [PATCH] 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