diff --git a/submodules/AvatarNode/Sources/AvatarNode.swift b/submodules/AvatarNode/Sources/AvatarNode.swift index 0f3e28f6b6..2789634af2 100644 --- a/submodules/AvatarNode/Sources/AvatarNode.swift +++ b/submodules/AvatarNode/Sources/AvatarNode.swift @@ -665,8 +665,8 @@ public final class AvatarNode: ASDisplayNode { } public let contentNode: ContentNode - private var storyIndicatorTheme: PresentationTheme? private var storyIndicator: ComponentView? + public private(set) var storyPresentationParams: StoryPresentationParams? public struct StoryStats: Equatable { public var totalCount: Int @@ -735,9 +735,7 @@ public final class AvatarNode: ASDisplayNode { guard let self else { return } - if let storyIndicatorTheme = self.storyIndicatorTheme { - self.updateStoryIndicator(theme: storyIndicatorTheme, transition: .immediate) - } + self.updateStoryIndicator(transition: .immediate) } self.addSubnode(self.contentNode) @@ -766,9 +764,7 @@ public final class AvatarNode: ASDisplayNode { self.contentNode.updateSize(size: size) - if let storyIndicatorTheme = self.storyIndicatorTheme { - self.updateStoryIndicator(theme: storyIndicatorTheme, transition: .immediate) - } + self.updateStoryIndicator(transition: .immediate) } public func playArchiveAnimation() { @@ -807,51 +803,71 @@ public final class AvatarNode: ASDisplayNode { self.contentNode.setCustomLetters(letters, explicitColor: explicitColor, icon: icon) } - public func setStoryStats(storyStats: StoryStats?, theme: PresentationTheme, transition: Transition) { - if self.storyStats != storyStats || self.storyIndicatorTheme !== theme { + public func setStoryStats(storyStats: StoryStats?, presentationParams: StoryPresentationParams, transition: Transition) { + if self.storyStats != storyStats || self.storyPresentationParams != presentationParams { self.storyStats = storyStats - self.storyIndicatorTheme = theme + self.storyPresentationParams = presentationParams - self.updateStoryIndicator(theme: theme, transition: transition) + self.updateStoryIndicator(transition: transition) } } - private struct StoryIndicatorParams { - let lineWidth: CGFloat - let indicatorSize: CGSize - let avatarScale: CGFloat + public struct Colors: Equatable { + public var unseenColors: [UIColor] + public var unseenCloseFriendsColors: [UIColor] + public var seenColors: [UIColor] - init(lineWidth: CGFloat, indicatorSize: CGSize, avatarScale: CGFloat) { + public init( + unseenColors: [UIColor], + unseenCloseFriendsColors: [UIColor], + seenColors: [UIColor] + ) { + self.unseenColors = unseenColors + self.unseenCloseFriendsColors = unseenCloseFriendsColors + self.seenColors = seenColors + } + + public init(theme: PresentationTheme) { + self.unseenColors = [theme.chatList.storyUnseenColors.topColor, theme.chatList.storyUnseenColors.bottomColor] + self.unseenCloseFriendsColors = [theme.chatList.storyUnseenPrivateColors.topColor, theme.chatList.storyUnseenPrivateColors.bottomColor] + self.seenColors = [theme.chatList.storySeenColors.topColor, theme.chatList.storySeenColors.bottomColor] + } + } + + public struct StoryPresentationParams: Equatable { + public var colors: Colors + public var lineWidth: CGFloat + public var inactiveLineWidth: CGFloat + + public init( + colors: Colors, + lineWidth: CGFloat, + inactiveLineWidth: CGFloat + ) { + self.colors = colors self.lineWidth = lineWidth - self.indicatorSize = indicatorSize - self.avatarScale = avatarScale + self.inactiveLineWidth = inactiveLineWidth } } - private func storyIndicatorParams(size: CGSize) -> StoryIndicatorParams { - let lineWidth: CGFloat = 2.0 - - return StoryIndicatorParams( - lineWidth: lineWidth, - indicatorSize: CGSize(width: size.width - lineWidth * 4.0, height: size.height - lineWidth * 4.0), - avatarScale: (size.width - lineWidth * 4.0) / size.width - ) - } - - private func updateStoryIndicator(theme: PresentationTheme, transition: Transition) { + private func updateStoryIndicator(transition: Transition) { if !self.isNodeLoaded { return } if self.bounds.isEmpty { return } - - self.storyIndicatorTheme = theme + guard let storyPresentationParams = self.storyPresentationParams else { + return + } let size = self.bounds.size if let storyStats = self.storyStats { - let indicatorParams = self.storyIndicatorParams(size: size) + let activeLineWidth = storyPresentationParams.lineWidth + let inactiveLineWidth = storyPresentationParams.inactiveLineWidth + let indicatorSize = CGSize(width: size.width - activeLineWidth * 4.0, height: size.height - activeLineWidth * 4.0) + let avatarScale = (size.width - activeLineWidth * 4.0) / size.width let storyIndicator: ComponentView var indicatorTransition = transition @@ -867,25 +883,28 @@ public final class AvatarNode: ASDisplayNode { component: AnyComponent(AvatarStoryIndicatorComponent( hasUnseen: storyStats.unseenCount != 0, hasUnseenCloseFriendsItems: storyStats.hasUnseenCloseFriendsItems, - theme: theme, - activeLineWidth: indicatorParams.lineWidth, - inactiveLineWidth: indicatorParams.lineWidth, - isGlassBackground: false, + colors: AvatarStoryIndicatorComponent.Colors( + unseenColors: storyPresentationParams.colors.unseenColors, + unseenCloseFriendsColors: storyPresentationParams.colors.unseenCloseFriendsColors, + seenColors: storyPresentationParams.colors.seenColors + ), + activeLineWidth: activeLineWidth, + inactiveLineWidth: inactiveLineWidth, counters: AvatarStoryIndicatorComponent.Counters( totalCount: storyStats.totalCount, unseenCount: storyStats.unseenCount ) )), environment: {}, - containerSize: indicatorParams.indicatorSize + containerSize: indicatorSize ) if let storyIndicatorView = storyIndicator.view { if storyIndicatorView.superview == nil { self.view.addSubview(storyIndicatorView) } - indicatorTransition.setFrame(view: storyIndicatorView, frame: CGRect(origin: CGPoint(x: (size.width - indicatorParams.indicatorSize.width) * 0.5, y: (size.height - indicatorParams.indicatorSize.height) * 0.5), size: indicatorParams.indicatorSize)) + indicatorTransition.setFrame(view: storyIndicatorView, frame: CGRect(origin: CGPoint(x: (size.width - indicatorSize.width) * 0.5, y: (size.height - indicatorSize.height) * 0.5), size: indicatorSize)) } - transition.setScale(view: self.contentNode.view, scale: indicatorParams.avatarScale) + transition.setScale(view: self.contentNode.view, scale: avatarScale) } else { transition.setScale(view: self.contentNode.view, scale: 1.0) if let storyIndicator = self.storyIndicator { diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 2cadcf8248..c1910f984d 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -2841,7 +2841,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { component: AnyComponent(AvatarStoryIndicatorComponent( hasUnseen: storyState.stats.unseenCount != 0, hasUnseenCloseFriendsItems: storyState.hasUnseenCloseFriends, - theme: item.presentationData.theme, + colors: AvatarStoryIndicatorComponent.Colors(theme: item.presentationData.theme), activeLineWidth: 2.33, inactiveLineWidth: 1.33, counters: AvatarStoryIndicatorComponent.Counters( diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index 81f89d6035..b28ee93038 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -1115,7 +1115,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { component: AnyComponent(AvatarStoryIndicatorComponent( hasUnseen: storyStats.unseen != 0, hasUnseenCloseFriendsItems: storyStats.hasUnseenCloseFriends, - theme: item.presentationData.theme, + colors: AvatarStoryIndicatorComponent.Colors(theme: item.presentationData.theme), activeLineWidth: 1.0 + UIScreenPixel, inactiveLineWidth: 1.0 + UIScreenPixel, counters: AvatarStoryIndicatorComponent.Counters(totalCount: storyStats.total, unseenCount: storyStats.unseen) diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift index 1aedfdac77..a4604011e7 100644 --- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift +++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift @@ -355,8 +355,10 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem { let shimmering: ItemListPeerItemShimmering? let displayDecorations: Bool let disableInteractiveTransitionIfNecessary: Bool + let storyStats: PeerStoryStats? + let openStories: ((UIView) -> Void)? - public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: EnginePeer, threadInfo: EngineMessageHistoryThread.Info? = nil, height: ItemListPeerItemHeight = .peerList, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, nameStyle: ItemListPeerItemNameStyle = .distinctBold, presence: EnginePeer.Presence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, highlighted: Bool = false, selectable: Bool, highlightable: Bool = true, animateFirstAvatarTransition: Bool = true, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, removePeer: @escaping (EnginePeer.Id) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, hasTopStripe: Bool = true, hasTopGroupInset: Bool = true, noInsets: Bool = false, noCorners: Bool = false, tag: ItemListItemTag? = nil, header: ListViewItemHeader? = nil, shimmering: ItemListPeerItemShimmering? = nil, displayDecorations: Bool = true, disableInteractiveTransitionIfNecessary: Bool = false) { + public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: EnginePeer, threadInfo: EngineMessageHistoryThread.Info? = nil, height: ItemListPeerItemHeight = .peerList, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, nameStyle: ItemListPeerItemNameStyle = .distinctBold, presence: EnginePeer.Presence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, highlighted: Bool = false, selectable: Bool, highlightable: Bool = true, animateFirstAvatarTransition: Bool = true, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, removePeer: @escaping (EnginePeer.Id) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, hasTopStripe: Bool = true, hasTopGroupInset: Bool = true, noInsets: Bool = false, noCorners: Bool = false, tag: ItemListItemTag? = nil, header: ListViewItemHeader? = nil, shimmering: ItemListPeerItemShimmering? = nil, displayDecorations: Bool = true, disableInteractiveTransitionIfNecessary: Bool = false, storyStats: PeerStoryStats? = nil, openStories: ((UIView) -> Void)? = nil) { self.presentationData = presentationData self.dateTimeFormat = dateTimeFormat self.nameDisplayOrder = nameDisplayOrder @@ -393,6 +395,8 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem { self.shimmering = shimmering self.displayDecorations = displayDecorations self.disableInteractiveTransitionIfNecessary = disableInteractiveTransitionIfNecessary + self.storyStats = storyStats + self.openStories = openStories } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -471,6 +475,8 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo private var avatarIconComponent: EmojiStatusComponent? private var avatarIconView: ComponentView? + private var avatarButton: HighlightTrackingButton? + private let titleNode: TextNode private let labelNode: TextNode private let labelBadgeNode: ASImageNode @@ -1250,6 +1256,22 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo let avatarFrame = CGRect(origin: CGPoint(x: params.leftInset + additionalLeftInset + revealOffset + editingOffset + 15.0, y: floorToScreenPixels((layout.contentSize.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)) transition.updateFrame(node: strongSelf.avatarNode, frame: avatarFrame) + if item.storyStats != nil { + let avatarButton: HighlightTrackingButton + if let current = strongSelf.avatarButton { + avatarButton = current + } else { + avatarButton = HighlightTrackingButton() + strongSelf.avatarButton = avatarButton + strongSelf.containerNode.view.addSubview(avatarButton) + avatarButton.addTarget(strongSelf, action: #selector(strongSelf.avatarButtonPressed), for: .touchUpInside) + } + avatarButton.frame = avatarFrame + } else if let avatarButton = strongSelf.avatarButton { + strongSelf.avatarButton = nil + avatarButton.removeFromSuperview() + } + if let switchValue = item.switchValue, case .leftCheck = switchValue.style { let leftCheckNode: CheckNode if let current = strongSelf.leftCheckNode { @@ -1332,6 +1354,17 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo clipStyle = .roundedRect } strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: item.peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, clipStyle: clipStyle, synchronousLoad: synchronousLoad) + strongSelf.avatarNode.setStoryStats(storyStats: item.storyStats.flatMap { storyStats in + return AvatarNode.StoryStats( + totalCount: storyStats.totalCount, + unseenCount: storyStats.unseenCount, + hasUnseenCloseFriendsItems: false + ) + }, presentationParams: AvatarNode.StoryPresentationParams( + colors: AvatarNode.Colors(theme: item.presentationData.theme), + lineWidth: 1.33, + inactiveLineWidth: 1.33 + ), transition: .immediate) } } @@ -1443,10 +1476,14 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { super.setHighlighted(highlighted, at: point, animated: animated) - - self.isHighlighted = highlighted + + if let avatarButton = self.avatarButton, avatarButton.bounds.contains(self.view.convert(point, to: avatarButton)) { + self.isHighlighted = false + } else { + self.isHighlighted = highlighted - self.updateIsHighlighted(transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate) + self.updateIsHighlighted(transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate) + } } override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { @@ -1513,7 +1550,11 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo transition.updateFrame(node: self.labelBadgeNode, frame: CGRect(origin: CGPoint(x: offset + params.width - rightLabelInset - badgeWidth, y: self.labelBadgeNode.frame.minY), size: CGSize(width: badgeWidth, height: badgeDiameter))) - transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + editingOffset + params.leftInset + 15.0, y: self.avatarNode.frame.minY), size: self.avatarNode.bounds.size)) + let avatarFrame = CGRect(origin: CGPoint(x: revealOffset + editingOffset + params.leftInset + 15.0, y: self.avatarNode.frame.minY), size: self.avatarNode.bounds.size) + transition.updateFrame(node: self.avatarNode, frame: avatarFrame) + if let avatarButton = self.avatarButton { + avatarButton.frame = avatarFrame + } if let avatarIconComponentView = self.avatarIconView?.view { let avatarFrame = self.avatarNode.frame @@ -1580,6 +1621,13 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo } return false } + + @objc private func avatarButtonPressed() { + guard let item = self.layoutParams?.0 else { + return + } + item.openStories?(self.avatarNode.view) + } } public final class ItemListPeerItemHeader: ListViewItemHeader { diff --git a/submodules/Postbox/Sources/PeerStoryStatsView.swift b/submodules/Postbox/Sources/PeerStoryStatsView.swift new file mode 100644 index 0000000000..5d55cdc9ac --- /dev/null +++ b/submodules/Postbox/Sources/PeerStoryStatsView.swift @@ -0,0 +1,76 @@ +import Foundation + +final class MutablePeerStoryStatsView: MutablePostboxView { + let peerIds: Set + var storyStats: [PeerId: PeerStoryStats] = [:] + + init(postbox: PostboxImpl, peerIds: Set) { + self.peerIds = peerIds + for id in self.peerIds { + if let value = fetchPeerStoryStats(postbox: postbox, peerId: id) { + self.storyStats[id] = value + } + } + } + + func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> Bool { + var updated = false + var updatedPeerIds = Set() + for event in transaction.currentStoryTopItemEvents { + if case let .replace(peerId) = event { + if self.peerIds.contains(peerId) { + updatedPeerIds.insert(peerId) + } + } + } + for event in transaction.storyPeerStatesEvents { + if case let .set(key) = event, case let .peer(peerId) = key { + if self.peerIds.contains(peerId) { + updatedPeerIds.insert(peerId) + } + } + } + + for id in updatedPeerIds { + let value = fetchPeerStoryStats(postbox: postbox, peerId: id) + if self.storyStats[id] != value { + updated = true + + if let value = value { + self.storyStats[id] = value + } else { + self.storyStats.removeValue(forKey: id) + } + } + } + + return updated + } + + func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool { + var storyStats: [PeerId: PeerStoryStats] = [:] + for id in self.peerIds { + if let value = fetchPeerStoryStats(postbox: postbox, peerId: id) { + storyStats[id] = value + } + } + if self.storyStats != storyStats { + self.storyStats = storyStats + return true + } else { + return false + } + } + + func immutableView() -> PostboxView { + return PeerStoryStatsView(self) + } +} + +public final class PeerStoryStatsView: PostboxView { + public let storyStats: [PeerId: PeerStoryStats] + + init(_ view: MutablePeerStoryStatsView) { + self.storyStats = view.storyStats + } +} diff --git a/submodules/Postbox/Sources/PeerView.swift b/submodules/Postbox/Sources/PeerView.swift index b9a3639216..ba0b94f90e 100644 --- a/submodules/Postbox/Sources/PeerView.swift +++ b/submodules/Postbox/Sources/PeerView.swift @@ -11,8 +11,9 @@ public struct PeerViewComponents: OptionSet { public static let subPeers = PeerViewComponents(rawValue: 1 << 1) public static let messages = PeerViewComponents(rawValue: 1 << 2) public static let groupId = PeerViewComponents(rawValue: 1 << 3) + public static let storyStats = PeerViewComponents(rawValue: 1 << 4) - public static let all: PeerViewComponents = [.cachedData, .subPeers, .messages, .groupId] + public static let all: PeerViewComponents = [.cachedData, .subPeers, .messages, .groupId, .storyStats] } final class MutablePeerView: MutablePostboxView { @@ -27,6 +28,8 @@ final class MutablePeerView: MutablePostboxView { var media: [MediaId: Media] = [:] var peerIsContact: Bool var groupId: PeerGroupId? + var storyStats: PeerStoryStats? + var memberStoryStats: [PeerId: PeerStoryStats] = [:] init(postbox: PostboxImpl, peerId: PeerId, components: PeerViewComponents) { self.components = components @@ -54,8 +57,10 @@ final class MutablePeerView: MutablePostboxView { } self.cachedData = postbox.cachedPeerDataTable.get(contactPeerId) self.peerIsContact = postbox.contactsTable.isContact(peerId: self.contactPeerId) + var cachedDataPeerIds = Set() if let cachedData = self.cachedData { - peerIds.formUnion(cachedData.peerIds) + cachedDataPeerIds = cachedData.peerIds + peerIds.formUnion(cachedDataPeerIds) messageIds.formUnion(cachedData.messageIds) } for id in peerIds { @@ -66,6 +71,11 @@ final class MutablePeerView: MutablePostboxView { self.peerPresences[id] = presence } } + for id in cachedDataPeerIds { + if let value = fetchPeerStoryStats(postbox: postbox, peerId: id) { + self.memberStoryStats[id] = value + } + } if let peer = self.peers[peerId], let associatedPeerId = peer.associatedPeerId { if let peer = getPeer(associatedPeerId) { self.peers[associatedPeerId] = peer @@ -83,6 +93,10 @@ final class MutablePeerView: MutablePostboxView { } } self.media = renderAssociatedMediaForPeers(postbox: postbox, peers: self.peers) + + if components.contains(.storyStats) { + self.storyStats = fetchPeerStoryStats(postbox: postbox, peerId: self.peerId) + } } func reset(postbox: PostboxImpl) -> Bool { @@ -260,6 +274,53 @@ final class MutablePeerView: MutablePostboxView { } } + if self.components.contains(.storyStats) { + var refreshStoryStats = false + for event in transaction.currentStoryTopItemEvents { + if case .replace(peerId: self.peerId) = event { + refreshStoryStats = true + } + } + if !refreshStoryStats { + for event in transaction.storyPeerStatesEvents { + if case .set(.peer(self.peerId)) = event { + refreshStoryStats = true + } + } + } + if refreshStoryStats { + self.storyStats = fetchPeerStoryStats(postbox: postbox, peerId: self.peerId) + } + } + + if !transaction.storyPeerStatesEvents.isEmpty || !transaction.currentStoryTopItemEvents.isEmpty { + if let cachedData = self.cachedData { + var updatedPeerIds = Set() + let cachedDataPeerIds = cachedData.peerIds + for event in transaction.currentStoryTopItemEvents { + if case let .replace(id) = event, cachedDataPeerIds.contains(id) { + updatedPeerIds.insert(id) + } + } + for event in transaction.storyPeerStatesEvents { + if case let .set(key) = event, case let .peer(id) = key, cachedDataPeerIds.contains(id) { + updatedPeerIds.insert(id) + } + } + for id in updatedPeerIds { + let value = fetchPeerStoryStats(postbox: postbox, peerId: id) + if self.memberStoryStats[id] != value { + updated = true + if let value = value { + self.memberStoryStats[id] = value + } else { + self.memberStoryStats.removeValue(forKey: id) + } + } + } + } + } + return updated } @@ -282,6 +343,8 @@ public final class PeerView: PostboxView { public let media: [MediaId: Media] public let peerIsContact: Bool public let groupId: PeerGroupId? + public let storyStats: PeerStoryStats? + public let memberStoryStats: [PeerId: PeerStoryStats] init(_ mutableView: MutablePeerView) { self.peerId = mutableView.peerId @@ -293,5 +356,7 @@ public final class PeerView: PostboxView { self.media = mutableView.media self.peerIsContact = mutableView.peerIsContact self.groupId = mutableView.groupId + self.storyStats = mutableView.storyStats + self.memberStoryStats = mutableView.memberStoryStats } } diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 40bae9cd61..175104a5da 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -1327,6 +1327,10 @@ public final class Transaction { public func getExpiredStoryIds(belowTimestamp: Int32) -> [StoryId] { return self.postbox!.storyItemsTable.getExpiredIds(belowTimestamp: belowTimestamp) } + + public func getPeerStoryStats(peerId: PeerId) -> PeerStoryStats? { + return fetchPeerStoryStats(postbox: self.postbox!, peerId: peerId) + } } public enum PostboxResult { diff --git a/submodules/Postbox/Sources/Views.swift b/submodules/Postbox/Sources/Views.swift index bab78e7fe1..f8f1904bf5 100644 --- a/submodules/Postbox/Sources/Views.swift +++ b/submodules/Postbox/Sources/Views.swift @@ -44,6 +44,7 @@ public enum PostboxViewKey: Hashable { case storiesState(key: PostboxStoryStatesKey) case storyItems(peerId: PeerId) case storyExpirationTimeItems + case peerStoryStats(peerIds: Set) public func hash(into hasher: inout Hasher) { switch self { @@ -147,6 +148,8 @@ public enum PostboxViewKey: Hashable { hasher.combine(peerId) case .storyExpirationTimeItems: hasher.combine(19) + case let .peerStoryStats(peerIds): + hasher.combine(peerIds) } } @@ -410,6 +413,12 @@ public enum PostboxViewKey: Hashable { } else { return false } + case let .peerStoryStats(peerIds): + if case .peerStoryStats(peerIds) = rhs { + return true + } else { + return false + } } } } @@ -502,5 +511,7 @@ func postboxViewForKey(postbox: PostboxImpl, key: PostboxViewKey) -> MutablePost return MutableStoryItemsView(postbox: postbox, peerId: peerId) case .storyExpirationTimeItems: return MutableStoryExpirationTimeItemsView(postbox: postbox) + case let .peerStoryStats(peerIds): + return MutablePeerStoryStatsView(postbox: postbox, peerIds: peerIds) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 5454973dc2..9e074a7733 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -1490,13 +1490,16 @@ public final class EngineStoryViewListContext { public final class Item: Equatable { public let peer: EnginePeer public let timestamp: Int32 + public let storyStats: PeerStoryStats? public init( peer: EnginePeer, - timestamp: Int32 + timestamp: Int32, + storyStats: PeerStoryStats? ) { self.peer = peer self.timestamp = timestamp + self.storyStats = storyStats } public static func ==(lhs: Item, rhs: Item) -> Bool { @@ -1506,6 +1509,9 @@ public final class EngineStoryViewListContext { if lhs.timestamp != rhs.timestamp { return false } + if lhs.storyStats != rhs.storyStats { + return false + } return true } } @@ -1545,6 +1551,7 @@ public final class EngineStoryViewListContext { let storyId: Int32 let disposable = MetaDisposable() + let storyStatsDisposable = MetaDisposable() var state: InternalState let statePromise = Promise() @@ -1569,6 +1576,7 @@ public final class EngineStoryViewListContext { assert(self.queue.isCurrent()) self.disposable.dispose() + self.storyStatsDisposable.dispose() } func loadMore() { @@ -1604,8 +1612,9 @@ public final class EngineStoryViewListContext { for view in views { switch view { case let .storyView(userId, date): - if let peer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))) { - items.append(Item(peer: EnginePeer(peer), timestamp: date)) + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) + if let peer = transaction.getPeer(peerId) { + items.append(Item(peer: EnginePeer(peer), timestamp: date, storyStats: transaction.getPeerStoryStats(peerId: peerId))) nextOffset = NextOffset(id: userId, timestamp: date) } @@ -1702,6 +1711,35 @@ public final class EngineStoryViewListContext { strongSelf.isLoadingMore = false strongSelf.statePromise.set(.single(strongSelf.state)) + + let statsKey: PostboxViewKey = .peerStoryStats(peerIds: Set(strongSelf.state.items.map(\.peer.id))) + strongSelf.storyStatsDisposable.set((strongSelf.account.postbox.combinedView(keys: [statsKey]) + |> deliverOn(strongSelf.queue)).start(next: { views in + guard let `self` = self else { + return + } + guard let view = views.views[statsKey] as? PeerStoryStatsView else { + return + } + var updated = false + var items = self.state.items + for i in 0 ..< strongSelf.state.items.count { + let item = items[i] + let value = view.storyStats[item.peer.id] + if item.storyStats != value { + updated = true + items[i] = Item( + peer: item.peer, + timestamp: item.timestamp, + storyStats: value + ) + } + } + if updated { + self.state.items = items + self.statePromise.set(.single(self.state)) + } + })) })) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift b/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift index 8c4a386330..44a09a8595 100644 --- a/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift @@ -214,7 +214,7 @@ public final class ChatAvatarNavigationNode: ASDisplayNode { component: AnyComponent(AvatarStoryIndicatorComponent( hasUnseen: storyData.hasUnseen, hasUnseenCloseFriendsItems: storyData.hasUnseenCloseFriends, - theme: theme, + colors: AvatarStoryIndicatorComponent.Colors(theme: theme), activeLineWidth: 1.0, inactiveLineWidth: 1.0, counters: nil diff --git a/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift b/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift index 2839f9364c..4b5ad7fdc2 100644 --- a/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift +++ b/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift @@ -5,6 +5,28 @@ import ComponentFlow import TelegramPresentationData public final class AvatarStoryIndicatorComponent: Component { + public struct Colors: Equatable { + public var unseenColors: [UIColor] + public var unseenCloseFriendsColors: [UIColor] + public var seenColors: [UIColor] + + public init( + unseenColors: [UIColor], + unseenCloseFriendsColors: [UIColor], + seenColors: [UIColor] + ) { + self.unseenColors = unseenColors + self.unseenCloseFriendsColors = unseenCloseFriendsColors + self.seenColors = seenColors + } + + public init(theme: PresentationTheme) { + self.unseenColors = [theme.chatList.storyUnseenColors.topColor, theme.chatList.storyUnseenColors.bottomColor] + self.unseenCloseFriendsColors = [theme.chatList.storyUnseenPrivateColors.topColor, theme.chatList.storyUnseenPrivateColors.bottomColor] + self.seenColors = [theme.chatList.storySeenColors.topColor, theme.chatList.storySeenColors.bottomColor] + } + } + public struct Counters: Equatable { public var totalCount: Int public var unseenCount: Int @@ -17,27 +39,24 @@ public final class AvatarStoryIndicatorComponent: Component { public let hasUnseen: Bool public let hasUnseenCloseFriendsItems: Bool - public let theme: PresentationTheme + public let colors: Colors public let activeLineWidth: CGFloat public let inactiveLineWidth: CGFloat - public let isGlassBackground: Bool public let counters: Counters? public init( hasUnseen: Bool, hasUnseenCloseFriendsItems: Bool, - theme: PresentationTheme, + colors: Colors, activeLineWidth: CGFloat, inactiveLineWidth: CGFloat, - isGlassBackground: Bool = false, counters: Counters? ) { self.hasUnseen = hasUnseen self.hasUnseenCloseFriendsItems = hasUnseenCloseFriendsItems - self.theme = theme + self.colors = colors self.activeLineWidth = activeLineWidth self.inactiveLineWidth = inactiveLineWidth - self.isGlassBackground = isGlassBackground self.counters = counters } @@ -48,7 +67,7 @@ public final class AvatarStoryIndicatorComponent: Component { if lhs.hasUnseenCloseFriendsItems != rhs.hasUnseenCloseFriendsItems { return false } - if lhs.theme !== rhs.theme { + if lhs.colors != rhs.colors { return false } if lhs.activeLineWidth != rhs.activeLineWidth { @@ -57,9 +76,6 @@ public final class AvatarStoryIndicatorComponent: Component { if lhs.inactiveLineWidth != rhs.inactiveLineWidth { return false } - if lhs.isGlassBackground != rhs.isGlassBackground { - return false - } if lhs.counters != rhs.counters { return false } @@ -101,16 +117,12 @@ public final class AvatarStoryIndicatorComponent: Component { let inactiveColors: [CGColor] if component.hasUnseenCloseFriendsItems { - activeColors = [component.theme.chatList.storyUnseenPrivateColors.topColor.cgColor, component.theme.chatList.storyUnseenPrivateColors.bottomColor.cgColor] + activeColors = component.colors.unseenCloseFriendsColors.map(\.cgColor) } else { - activeColors = [component.theme.chatList.storyUnseenColors.topColor.cgColor, component.theme.chatList.storyUnseenColors.bottomColor.cgColor] + activeColors = component.colors.unseenColors.map(\.cgColor) } - if component.isGlassBackground { - inactiveColors = [UIColor(white: 1.0, alpha: 0.2).cgColor, UIColor(white: 1.0, alpha: 0.2).cgColor] - } else { - inactiveColors = [component.theme.chatList.storySeenColors.topColor.cgColor, component.theme.chatList.storySeenColors.bottomColor.cgColor] - } + inactiveColors = component.colors.seenColors.map(\.cgColor) var locations: [CGFloat] = [0.0, 1.0] diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/BUILD b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/BUILD index f5842622ea..d9bf6ecfdd 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/BUILD +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/BUILD @@ -17,6 +17,7 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit", "//submodules/AccountContext", "//submodules/TelegramCore", + "//submodules/Postbox", "//submodules/Components/MultilineTextComponent", "//submodules/AvatarNode", "//submodules/CheckNode", diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift index 5662bfb783..283da79577 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift @@ -6,6 +6,7 @@ import ComponentFlow import SwiftSignalKit import AccountContext import TelegramCore +import Postbox import MultilineTextComponent import AvatarNode import TelegramPresentationData @@ -49,12 +50,14 @@ public final class PeerListItemComponent: Component { let sideInset: CGFloat let title: String let peer: EnginePeer? + let storyStats: PeerStoryStats? let subtitle: String? let subtitleAccessory: SubtitleAccessory let presence: EnginePeer.Presence? let selectionState: SelectionState let hasNext: Bool let action: (EnginePeer) -> Void + let openStories: ((EnginePeer, UIView) -> Void)? public init( context: AccountContext, @@ -64,12 +67,14 @@ public final class PeerListItemComponent: Component { sideInset: CGFloat, title: String, peer: EnginePeer?, + storyStats: PeerStoryStats? = nil, subtitle: String?, subtitleAccessory: SubtitleAccessory, presence: EnginePeer.Presence?, selectionState: SelectionState, hasNext: Bool, - action: @escaping (EnginePeer) -> Void + action: @escaping (EnginePeer) -> Void, + openStories: ((EnginePeer, UIView) -> Void)? = nil ) { self.context = context self.theme = theme @@ -78,12 +83,14 @@ public final class PeerListItemComponent: Component { self.sideInset = sideInset self.title = title self.peer = peer + self.storyStats = storyStats self.subtitle = subtitle self.subtitleAccessory = subtitleAccessory self.presence = presence self.selectionState = selectionState self.hasNext = hasNext self.action = action + self.openStories = openStories } public static func ==(lhs: PeerListItemComponent, rhs: PeerListItemComponent) -> Bool { @@ -108,6 +115,9 @@ public final class PeerListItemComponent: Component { if lhs.peer != rhs.peer { return false } + if lhs.storyStats != rhs.storyStats { + return false + } if lhs.subtitle != rhs.subtitle { return false } @@ -133,6 +143,7 @@ public final class PeerListItemComponent: Component { private let label = ComponentView() private let separatorLayer: SimpleLayer private let avatarNode: AvatarNode + private let avatarButtonView: HighlightTrackingButton private var avatarIcon: ComponentView? private var iconView: UIImageView? @@ -168,7 +179,9 @@ public final class PeerListItemComponent: Component { self.containerButton = HighlightTrackingButton() self.avatarNode = AvatarNode(font: avatarFont) - self.avatarNode.isLayerBacked = true + self.avatarNode.isLayerBacked = false + + self.avatarButtonView = HighlightTrackingButton() super.init(frame: frame) @@ -177,6 +190,9 @@ public final class PeerListItemComponent: Component { self.containerButton.layer.addSublayer(self.avatarNode.layer) self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + + self.addSubview(self.avatarButtonView) + self.avatarButtonView.addTarget(self, action: #selector(self.avatarButtonPressed), for: .touchUpInside) } required init?(coder: NSCoder) { @@ -190,6 +206,13 @@ public final class PeerListItemComponent: Component { component.action(peer) } + @objc private func avatarButtonPressed() { + guard let component = self.component, let peer = component.peer else { + return + } + component.openStories?(peer, self.avatarNode.view) + } + func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { var synchronousLoad = false if let hint = transition.userData(TransitionHint.self) { @@ -234,6 +257,8 @@ public final class PeerListItemComponent: Component { self.component = component self.state = state + self.avatarButtonView.isUserInteractionEnabled = component.storyStats != nil + let labelData: (String, Bool) if let presence = component.presence { let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 @@ -319,6 +344,8 @@ public final class PeerListItemComponent: Component { transition.setFrame(layer: self.avatarNode.layer, frame: avatarFrame) } + transition.setFrame(view: self.avatarButtonView, frame: avatarFrame) + var statusIcon: EmojiStatusComponent.Content? if let peer = component.peer { let clipStyle: AvatarNodeClipStyle @@ -328,6 +355,17 @@ public final class PeerListItemComponent: Component { clipStyle = .round } self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: clipStyle, synchronousLoad: synchronousLoad, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) + self.avatarNode.setStoryStats(storyStats: component.storyStats.flatMap { storyStats -> AvatarNode.StoryStats in + return AvatarNode.StoryStats( + totalCount: storyStats.totalCount == 0 ? 0 : 1, + unseenCount: storyStats.unseenCount == 0 ? 0 : 1, + hasUnseenCloseFriendsItems: false + ) + }, presentationParams: AvatarNode.StoryPresentationParams( + colors: AvatarNode.Colors(theme: component.theme), + lineWidth: 1.33, + inactiveLineWidth: 1.33 + ), transition: transition) if peer.isScam { statusIcon = .text(color: component.theme.chat.message.incoming.scamColor, string: component.strings.Message_ScamAccount.uppercased()) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 028eb41b74..836a02bf84 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -2039,6 +2039,12 @@ public final class StoryItemSetContainerComponent: Component { return } self.navigateToPeer(peer: peer, chat: false) + }, + openPeerStories: { [weak self] peer, sourceView in + guard let self else { + return + } + self.openPeerStories(peer: peer, sourceView: sourceView) } )), environment: {}, @@ -3163,6 +3169,77 @@ public final class StoryItemSetContainerComponent: Component { } } + func openPeerStories(peer: EnginePeer, sourceView: UIView) { + guard let component = self.component else { + return + } + + let storyContent = StoryContentContextImpl(context: component.context, isHidden: false, focusedPeerId: peer.id, singlePeer: true) + let _ = (storyContent.state + |> filter { $0.slice != nil } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self, weak sourceView] _ in + guard let self, let component = self.component else { + return + } + + var transitionIn: StoryContainerScreen.TransitionIn? + if let sourceView { + transitionIn = StoryContainerScreen.TransitionIn( + sourceView: sourceView, + sourceRect: sourceView.bounds, + sourceCornerRadius: sourceView.bounds.width * 0.5, + sourceIsAvatar: false + ) + sourceView.isHidden = true + } + + let storyContainerScreen = StoryContainerScreen( + context: component.context, + content: storyContent, + transitionIn: transitionIn, + transitionOut: { peerId, _ in + if let sourceView { + let destinationView = sourceView + return StoryContainerScreen.TransitionOut( + destinationView: destinationView, + transitionView: StoryContainerScreen.TransitionView( + makeView: { [weak destinationView] in + let parentView = UIView() + if let copyView = destinationView?.snapshotContentTree(unhide: true) { + parentView.addSubview(copyView) + } + return parentView + }, + updateView: { copyView, state, transition in + guard let view = copyView.subviews.first else { + return + } + let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress) + transition.setPosition(view: view, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) + transition.setScale(view: view, scale: size.width / state.destinationSize.width) + }, + insertCloneTransitionView: nil + ), + destinationRect: destinationView.bounds, + destinationCornerRadius: destinationView.bounds.width * 0.5, + destinationIsAvatar: false, + completed: { [weak sourceView] in + guard let sourceView else { + return + } + sourceView.isHidden = false + } + ) + } else { + return nil + } + } + ) + component.controller()?.push(storyContainerScreen) + }) + } + private func openStoryEditing() { guard let component = self.component, let peerReference = PeerReference(component.slice.peer._asPeer()) else { return diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift index 7f0a4da61f..9ad618697a 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift @@ -56,6 +56,7 @@ final class StoryItemSetViewListComponent: Component { let deleteAction: () -> Void let moreAction: (UIView, ContextGesture?) -> Void let openPeer: (EnginePeer) -> Void + let openPeerStories: (EnginePeer, UIView) -> Void init( externalState: ExternalState, @@ -73,7 +74,8 @@ final class StoryItemSetViewListComponent: Component { expandViewStats: @escaping () -> Void, deleteAction: @escaping () -> Void, moreAction: @escaping (UIView, ContextGesture?) -> Void, - openPeer: @escaping (EnginePeer) -> Void + openPeer: @escaping (EnginePeer) -> Void, + openPeerStories: @escaping (EnginePeer, UIView) -> Void ) { self.externalState = externalState self.context = context @@ -91,6 +93,7 @@ final class StoryItemSetViewListComponent: Component { self.deleteAction = deleteAction self.moreAction = moreAction self.openPeer = openPeer + self.openPeerStories = openPeerStories } static func ==(lhs: StoryItemSetViewListComponent, rhs: StoryItemSetViewListComponent) -> Bool { @@ -484,6 +487,7 @@ final class StoryItemSetViewListComponent: Component { sideInset: 0.0, title: item.peer.displayTitle(strings: component.strings, displayOrder: .firstLast), peer: item.peer, + storyStats: item.storyStats, subtitle: dateText, subtitleAccessory: .checks, presence: nil, @@ -494,6 +498,12 @@ final class StoryItemSetViewListComponent: Component { return } component.openPeer(peer) + }, + openStories: { [weak self] peer, sourceView in + guard let self, let component = self.component else { + return + } + component.openPeerStories(peer, sourceView) } )), environment: {}, diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 9a7412e760..7fa1ce8fb2 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -563,6 +563,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private var powerSavingMonitoringDisposable: Disposable? private var avatarNode: ChatAvatarNavigationNode? + private var storyStats: PeerStoryStats? public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, botAppStart: ChatControllerInitialBotAppStart? = nil, mode: ChatControllerPresentationMode = .standard(previewing: false), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [ChatNavigationStackItem] = []) { let _ = ChatControllerCount.modify { value in @@ -1165,7 +1166,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var expandAvatar = false if case let .groupParticipant(storyStats, avatarHeaderNode) = source { if let storyStats, storyStats.totalCount != 0, let avatarHeaderNode = avatarHeaderNode as? ChatMessageAvatarHeaderNode { - self?.openStories(peerId: peer.id, avatarHeaderNode: avatarHeaderNode) + self?.openStories(peerId: peer.id, avatarHeaderNode: avatarHeaderNode, avatarNode: nil) return } else { expandAvatar = true @@ -4708,8 +4709,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G chatInfoButtonItem = UIBarButtonItem(customDisplayNode: avatarNode)! self.avatarNode = avatarNode - - //avatarNode.updateStoryView(transition: .immediate, theme: self.presentationData.theme) case .feed: chatInfoButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) } @@ -4998,6 +4997,24 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.setPeer(context: strongSelf.context, theme: strongSelf.presentationData.theme, peer: EnginePeer(peer), overrideImage: imageOverride) (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.contextActionIsEnabled = strongSelf.chatLocation.threadId == nil && peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil strongSelf.chatInfoNavigationButton?.buttonItem.accessibilityLabel = presentationInterfaceState.strings.Conversation_ContextMenuOpenProfile + + strongSelf.storyStats = peerView.storyStats + if let avatarNode = strongSelf.avatarNode { + avatarNode.avatarNode.setStoryStats(storyStats: peerView.storyStats.flatMap { storyStats -> AvatarNode.StoryStats? in + if storyStats.totalCount == 0 { + return nil + } + return AvatarNode.StoryStats( + totalCount: storyStats.totalCount == 0 ? 0 : 1, + unseenCount: storyStats.unseenCount == 0 ? 0 : 1, + hasUnseenCloseFriendsItems: false + ) + }, presentationParams: AvatarNode.StoryPresentationParams( + colors: AvatarNode.Colors(theme: strongSelf.presentationData.theme), + lineWidth: 1.5, + inactiveLineWidth: 1.5 + ), transition: .immediate) + } } } } @@ -12230,7 +12247,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G @objc func rightNavigationButtonAction() { if let button = self.rightNavigationButton { - self.navigationButtonAction(button.action) + if case let .peer(peerId) = self.chatLocation, case .openChatInfo(expandAvatar: true) = button.action, let storyStats = self.storyStats, storyStats.totalCount != 0, let avatarNode = self.avatarNode { + self.openStories(peerId: peerId, avatarHeaderNode: nil, avatarNode: avatarNode.avatarNode) + } else { + self.navigationButtonAction(button.action) + } } } @@ -17011,12 +17032,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - private func openStories(peerId: EnginePeer.Id, avatarHeaderNode: ChatMessageAvatarHeaderNode) { + private func openStories(peerId: EnginePeer.Id, avatarHeaderNode: ChatMessageAvatarHeaderNode?, avatarNode: AvatarNode?) { let storyContent = StoryContentContextImpl(context: self.context, isHidden: false, focusedPeerId: peerId, singlePeer: true) let _ = (storyContent.state |> filter { $0.slice != nil } |> take(1) - |> deliverOnMainQueue).start(next: { [weak self, weak avatarHeaderNode] _ in + |> deliverOnMainQueue).start(next: { [weak self, weak avatarHeaderNode, weak avatarNode] _ in guard let self else { return } @@ -17030,6 +17051,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G sourceIsAvatar: false ) avatarHeaderNode.avatarNode.isHidden = true + } else if let avatarNode { + transitionIn = StoryContainerScreen.TransitionIn( + sourceView: avatarNode.view, + sourceRect: avatarNode.view.bounds, + sourceCornerRadius: avatarNode.view.bounds.width * 0.5, + sourceIsAvatar: false + ) + avatarNode.isHidden = true } let storyContainerScreen = StoryContainerScreen( @@ -17037,40 +17066,73 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G content: storyContent, transitionIn: transitionIn, transitionOut: { peerId, _ in - guard let avatarHeaderNode else { - return nil - } - let destinationView = avatarHeaderNode.avatarNode.view - return StoryContainerScreen.TransitionOut( - destinationView: destinationView, - transitionView: StoryContainerScreen.TransitionView( - makeView: { [weak destinationView] in - let parentView = UIView() - if let copyView = destinationView?.snapshotContentTree(unhide: true) { - parentView.addSubview(copyView) - } - return parentView - }, - updateView: { copyView, state, transition in - guard let view = copyView.subviews.first else { + if let avatarHeaderNode { + let destinationView = avatarHeaderNode.avatarNode.view + return StoryContainerScreen.TransitionOut( + destinationView: destinationView, + transitionView: StoryContainerScreen.TransitionView( + makeView: { [weak destinationView] in + let parentView = UIView() + if let copyView = destinationView?.snapshotContentTree(unhide: true) { + parentView.addSubview(copyView) + } + return parentView + }, + updateView: { copyView, state, transition in + guard let view = copyView.subviews.first else { + return + } + let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress) + transition.setPosition(view: view, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) + transition.setScale(view: view, scale: size.width / state.destinationSize.width) + }, + insertCloneTransitionView: nil + ), + destinationRect: destinationView.bounds, + destinationCornerRadius: destinationView.bounds.width * 0.5, + destinationIsAvatar: false, + completed: { [weak avatarHeaderNode] in + guard let avatarHeaderNode else { return } - let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress) - transition.setPosition(view: view, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) - transition.setScale(view: view, scale: size.width / state.destinationSize.width) - }, - insertCloneTransitionView: nil - ), - destinationRect: destinationView.bounds, - destinationCornerRadius: destinationView.bounds.width * 0.5, - destinationIsAvatar: false, - completed: { [weak avatarHeaderNode] in - guard let avatarHeaderNode else { - return + avatarHeaderNode.avatarNode.isHidden = false } - avatarHeaderNode.avatarNode.isHidden = false - } - ) + ) + } else if let avatarNode { + let destinationView = avatarNode.view + return StoryContainerScreen.TransitionOut( + destinationView: destinationView, + transitionView: StoryContainerScreen.TransitionView( + makeView: { [weak destinationView] in + let parentView = UIView() + if let copyView = destinationView?.snapshotContentTree(unhide: true) { + parentView.addSubview(copyView) + } + return parentView + }, + updateView: { copyView, state, transition in + guard let view = copyView.subviews.first else { + return + } + let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress) + transition.setPosition(view: view, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) + transition.setScale(view: view, scale: size.width / state.destinationSize.width) + }, + insertCloneTransitionView: nil + ), + destinationRect: destinationView.bounds, + destinationCornerRadius: destinationView.bounds.width * 0.5, + destinationIsAvatar: false, + completed: { [weak avatarNode] in + guard let avatarNode else { + return + } + avatarNode.isHidden = false + } + ) + } else { + return nil + } } ) self.push(storyContainerScreen) diff --git a/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift b/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift index 914e021087..109d552e69 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift +++ b/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift @@ -613,7 +613,11 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode { unseenCount: storyStats.unseenCount, hasUnseenCloseFriendsItems: false ) - }, theme: theme, transition: .immediate) + }, presentationParams: AvatarNode.StoryPresentationParams( + colors: AvatarNode.Colors(theme: theme), + lineWidth: 2.0, + inactiveLineWidth: 2.0 + ), transition: .immediate) } } diff --git a/submodules/TelegramUI/Sources/ChatMessageStoryMentionContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageStoryMentionContentNode.swift index 0c08bfc796..c98b83e36c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStoryMentionContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStoryMentionContentNode.swift @@ -278,15 +278,16 @@ class ChatMessageStoryMentionContentNode: ChatMessageBubbleContentNode { } let indicatorFrame = imageFrame + var storyColors = AvatarStoryIndicatorComponent.Colors(theme: item.presentationData.theme.theme) + storyColors.seenColors = [UIColor(white: 1.0, alpha: 0.2), UIColor(white: 1.0, alpha: 0.2)] let _ = strongSelf.storyIndicator.update( transition: .immediate, component: AnyComponent(AvatarStoryIndicatorComponent( hasUnseen: hasUnseen, hasUnseenCloseFriendsItems: hasUnseen && (story?.isCloseFriends ?? false), - theme: item.presentationData.theme.theme, + colors: storyColors, activeLineWidth: 3.0, inactiveLineWidth: 1.0 + UIScreenPixel, - isGlassBackground: true, counters: nil )), environment: {}, diff --git a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenMemberItem.swift b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenMemberItem.swift index 7a089aabf9..c59d8cb50f 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenMemberItem.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenMemberItem.swift @@ -26,6 +26,7 @@ final class PeerInfoScreenMemberItem: PeerInfoScreenItem { let isAccount: Bool let action: ((PeerInfoScreenMemberItemAction) -> Void)? let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? + let openStories: ((UIView) -> Void)? init( id: AnyHashable, @@ -35,7 +36,8 @@ final class PeerInfoScreenMemberItem: PeerInfoScreenItem { badge: String? = nil, isAccount: Bool, action: ((PeerInfoScreenMemberItemAction) -> Void)?, - contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil + contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, + openStories: ((UIView) -> Void)? = nil ) { self.id = id self.context = context @@ -45,6 +47,7 @@ final class PeerInfoScreenMemberItem: PeerInfoScreenItem { self.isAccount = isAccount self.action = action self.contextAction = contextAction + self.openStories = openStories } func node() -> PeerInfoScreenItemNode { @@ -195,7 +198,12 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode { }, removePeer: { _ in - }, contextAction: item.contextAction, hasTopStripe: false, hasTopGroupInset: false, noInsets: true, noCorners: true, displayDecorations: false) + }, contextAction: item.contextAction, hasTopStripe: false, hasTopGroupInset: false, noInsets: true, noCorners: true, displayDecorations: false, storyStats: item.member.storyStats, openStories: { [weak self] sourceView in + guard let self, let item = self.item else { + return + } + item.openStories?(sourceView) + }) let params = ListViewItemLayoutParams(width: width, leftInset: safeInsets.left, rightInset: safeInsets.right, availableHeight: 1000.0) diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoMembersPane.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoMembersPane.swift index 3a9c6ed642..09b9a01d1f 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoMembersPane.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoMembersPane.swift @@ -26,6 +26,7 @@ enum PeerMembersListAction { case promote case restrict case remove + case openStories(sourceView: UIView) } private enum PeerMembersListEntryStableId: Hashable { @@ -35,7 +36,7 @@ private enum PeerMembersListEntryStableId: Hashable { private enum PeerMembersListEntry: Comparable, Identifiable { case addMember(PresentationTheme, String) - case member(PresentationTheme, Int, PeerInfoMember) + case member(theme: PresentationTheme, index: Int, member: PeerInfoMember) var stableId: PeerMembersListEntryStableId { switch self { @@ -126,7 +127,9 @@ private enum PeerMembersListEntry: Comparable, Identifiable { action(member, .open) }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in - }, contextAction: nil, hasTopStripe: false, noInsets: true, noCorners: true, disableInteractiveTransitionIfNecessary: true) + }, contextAction: nil, hasTopStripe: false, noInsets: true, noCorners: true, disableInteractiveTransitionIfNecessary: true, storyStats: member.storyStats, openStories: { sourceView in + action(member, .openStories(sourceView: sourceView)) + }) } } } @@ -265,7 +268,7 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { entries.append(.addMember(presentationData.theme, presentationData.strings.GroupInfo_AddParticipant)) } for member in state.members { - entries.append(.member(presentationData.theme, entries.count, member)) + entries.append(.member(theme: presentationData.theme, index: entries.count, member: member)) } let transaction = preparedTransition(from: self.currentEntries, to: entries, context: self.context, presentationData: presentationData, enclosingPeer: enclosingPeer, addMemberAction: { [weak self] in diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift index ff334bb932..221807deb0 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift @@ -1106,7 +1106,7 @@ func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: Peer?, member: result.insert(.promote) } else { switch member { - case let .channelMember(channelMember): + case let .channelMember(channelMember, _): switch channelMember.participant { case .creator: break @@ -1142,7 +1142,7 @@ func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: Peer?, member: result.insert(.promote) case .admin: switch member { - case let .legacyGroupMember(_, _, invitedBy, _): + case let .legacyGroupMember(_, _, invitedBy, _, _): result.insert(.restrict) if invitedBy == accountPeerId { result.insert(.promote) @@ -1154,7 +1154,7 @@ func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: Peer?, member: } case .member: switch member { - case let .legacyGroupMember(_, _, invitedBy, _): + case let .legacyGroupMember(_, _, invitedBy, _, _): if invitedBy == accountPeerId { result.insert(.restrict) } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index a4dba03b70..db5bef87f0 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -379,13 +379,15 @@ final class PeerInfoHeaderNavigationTransition { let sourceTitleView: ChatTitleView let sourceTitleFrame: CGRect let sourceSubtitleFrame: CGRect + let previousAvatarView: UIView? let fraction: CGFloat - init(sourceNavigationBar: NavigationBar, sourceTitleView: ChatTitleView, sourceTitleFrame: CGRect, sourceSubtitleFrame: CGRect, fraction: CGFloat) { + init(sourceNavigationBar: NavigationBar, sourceTitleView: ChatTitleView, sourceTitleFrame: CGRect, sourceSubtitleFrame: CGRect, previousAvatarView: UIView?, fraction: CGFloat) { self.sourceNavigationBar = sourceNavigationBar self.sourceTitleView = sourceTitleView self.sourceTitleFrame = sourceTitleFrame self.sourceSubtitleFrame = sourceSubtitleFrame + self.previousAvatarView = previousAvatarView self.fraction = fraction } } @@ -455,40 +457,22 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { } func updateStoryView(transition: ContainedViewLayoutTransition, theme: PresentationTheme) { - if let storyData = self.storyData { - let avatarStoryView: ComponentView - if let current = self.avatarStoryView { - avatarStoryView = current - } else { - avatarStoryView = ComponentView() - self.avatarStoryView = avatarStoryView - } - - let _ = avatarStoryView.update( - transition: Transition(transition), - component: AnyComponent(AvatarStoryIndicatorComponent( - hasUnseen: storyData.hasUnseen, - hasUnseenCloseFriendsItems: storyData.hasUnseenCloseFriends, - theme: theme, - activeLineWidth: 3.0, - inactiveLineWidth: 2.0, - counters: nil - )), - environment: {}, - containerSize: self.avatarNode.bounds.size + var colors = AvatarNode.Colors(theme: theme) + colors.seenColors = [ + theme.list.controlSecondaryColor, + theme.list.controlSecondaryColor + ] + self.avatarNode.setStoryStats(storyStats: self.storyData.flatMap { storyData in + return AvatarNode.StoryStats( + totalCount: 1, + unseenCount: storyData.hasUnseen ? 1 : 0, + hasUnseenCloseFriendsItems: storyData.hasUnseenCloseFriends ) - if let avatarStoryComponentView = avatarStoryView.view { - if avatarStoryComponentView.superview == nil { - self.containerNode.view.insertSubview(avatarStoryComponentView, at: 0) - } - avatarStoryComponentView.frame = self.avatarNode.frame - } - } else { - if let avatarStoryView = self.avatarStoryView { - self.avatarStoryView = nil - avatarStoryView.view?.removeFromSuperview() - } - } + }, presentationParams: AvatarNode.StoryPresentationParams( + colors: colors, + lineWidth: 3.0, + inactiveLineWidth: 1.5 + ), transition: Transition(transition)) } @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { @@ -610,11 +594,11 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { avatarCornerRadius = avatarSize / 2.0 } if self.avatarNode.layer.cornerRadius != 0.0 { - ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut).updateCornerRadius(layer: self.avatarNode.layer, cornerRadius: avatarCornerRadius) + ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut).updateCornerRadius(layer: self.avatarNode.contentNode.layer, cornerRadius: avatarCornerRadius) } else { - self.avatarNode.layer.cornerRadius = avatarCornerRadius + self.avatarNode.contentNode.layer.cornerRadius = avatarCornerRadius } - self.avatarNode.layer.masksToBounds = true + self.avatarNode.contentNode.layer.masksToBounds = true self.isFirstAvatarLoading = false @@ -1175,7 +1159,6 @@ final class PeerInfoAvatarListNode: ASDisplayNode { self.containerNode = ASDisplayNode() self.bottomCoverNode = ASDisplayNode() - self.bottomCoverNode.backgroundColor = .black self.maskNode = DynamicIslandMaskNode() self.pinchSourceNode = PinchSourceContainerNode() @@ -2918,6 +2901,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { } else { transitionSourceAvatarFrame = avatarNavigationNode.avatarNode.view.convert(avatarNavigationNode.avatarNode.view.bounds, to: navigationTransition.sourceNavigationBar.view) } + transition.updateAlpha(node: self.avatarListNode.avatarContainerNode.avatarNode, alpha: 1.0 - transitionFraction) } else { if deviceMetrics.hasDynamicIsland && !isLandscape { transitionSourceAvatarFrame = CGRect(origin: CGPoint(x: avatarFrame.minX, y: -20.0), size: avatarFrame.size).insetBy(dx: avatarSize * 0.4, dy: avatarSize * 0.4) @@ -3370,7 +3354,13 @@ final class PeerInfoHeaderNode: ASDisplayNode { let avatarOffset: CGFloat if self.navigationTransition != nil { if let transitionSourceAvatarFrame = transitionSourceAvatarFrame { - avatarScale = ((1.0 - transitionFraction) * avatarFrame.width + transitionFraction * transitionSourceAvatarFrame.width) / avatarFrame.width + var trueAvatarSize = transitionSourceAvatarFrame.size + if self.avatarListNode.avatarContainerNode.avatarNode.storyStats != nil { + trueAvatarSize.width -= 1.33 * 4.0 + trueAvatarSize.height -= 1.33 * 4.0 + } + + avatarScale = ((1.0 - transitionFraction) * avatarFrame.width + transitionFraction * trueAvatarSize.width) / avatarFrame.width } else { avatarScale = 1.0 } @@ -3380,6 +3370,14 @@ final class PeerInfoHeaderNode: ASDisplayNode { avatarOffset = apparentTitleLockOffset + 0.0 * (1.0 - titleCollapseFraction) + 10.0 * titleCollapseFraction } + if let previousAvatarView = self.navigationTransition?.previousAvatarView, let transitionSourceAvatarFrame { + let previousScale = ((1.0 - transitionFraction) * avatarFrame.width + transitionFraction * transitionSourceAvatarFrame.width) / transitionSourceAvatarFrame.width + + transition.updateAlpha(layer: previousAvatarView.layer, alpha: transitionFraction) + transition.updateTransformScale(layer: previousAvatarView.layer, scale: previousScale) + transition.updatePosition(layer: previousAvatarView.layer, position: self.view.convert(CGPoint(x: avatarCenter.x - (27.0 * (1.0 - transitionFraction) + 10 * transitionFraction), y: avatarCenter.y - (2.66 * (1.0 - transitionFraction) + 1.0 * transitionFraction)), to: previousAvatarView.superview)) + } + if subtitleIsButton { subtitleFrame.origin.y += 11.0 * (1.0 - titleCollapseFraction) if let subtitleBackgroundButton = self.subtitleBackgroundButton { @@ -3398,8 +3396,14 @@ final class PeerInfoHeaderNode: ASDisplayNode { if self.isAvatarExpanded { self.avatarListNode.listContainerNode.isHidden = false if let transitionSourceAvatarFrame = transitionSourceAvatarFrame { - transition.updateCornerRadius(node: self.avatarListNode.listContainerNode, cornerRadius: transitionFraction * transitionSourceAvatarFrame.width / 2.0) - transition.updateCornerRadius(node: self.avatarListNode.listContainerNode.controlsClippingNode, cornerRadius: transitionFraction * transitionSourceAvatarFrame.width / 2.0) + var trueAvatarSize = transitionSourceAvatarFrame.size + if self.avatarListNode.avatarContainerNode.avatarNode.storyStats != nil { + trueAvatarSize.width -= 1.33 * 4.0 + trueAvatarSize.height -= 1.33 * 4.0 + } + + transition.updateCornerRadius(node: self.avatarListNode.listContainerNode, cornerRadius: transitionFraction * trueAvatarSize.width / 2.0) + transition.updateCornerRadius(node: self.avatarListNode.listContainerNode.controlsClippingNode, cornerRadius: transitionFraction * trueAvatarSize.width / 2.0) } else { transition.updateCornerRadius(node: self.avatarListNode.listContainerNode, cornerRadius: 0.0) transition.updateCornerRadius(node: self.avatarListNode.listContainerNode.controlsClippingNode, cornerRadius: 0.0) @@ -3435,19 +3439,31 @@ final class PeerInfoHeaderNode: ASDisplayNode { transition.updateAlpha(layer: avatarStoryView.layer, alpha: 1.0 - transitionFraction) } - let apparentAvatarFrame: CGRect + var apparentAvatarFrame: CGRect let controlsClippingFrame: CGRect if self.isAvatarExpanded { let expandedAvatarCenter = CGPoint(x: expandedAvatarListSize.width / 2.0, y: expandedAvatarListSize.height / 2.0 - contentOffset / 2.0) apparentAvatarFrame = CGRect(origin: CGPoint(x: expandedAvatarCenter.x * (1.0 - transitionFraction) + transitionFraction * avatarCenter.x, y: expandedAvatarCenter.y * (1.0 - transitionFraction) + transitionFraction * avatarCenter.y), size: CGSize()) if let transitionSourceAvatarFrame = transitionSourceAvatarFrame { + var trueAvatarSize = transitionSourceAvatarFrame.size + if self.avatarListNode.avatarContainerNode.avatarNode.storyStats != nil { + trueAvatarSize.width -= 1.33 * 4.0 + trueAvatarSize.height -= 1.33 * 4.0 + } + let trueAvatarFrame = trueAvatarSize.centered(around: transitionSourceAvatarFrame.center) + let expandedFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: expandedAvatarListSize) - controlsClippingFrame = CGRect(origin: CGPoint(x: transitionFraction * transitionSourceAvatarFrame.minX + (1.0 - transitionFraction) * expandedFrame.minX, y: transitionFraction * transitionSourceAvatarFrame.minY + (1.0 - transitionFraction) * expandedFrame.minY), size: CGSize(width: transitionFraction * transitionSourceAvatarFrame.width + (1.0 - transitionFraction) * expandedFrame.width, height: transitionFraction * transitionSourceAvatarFrame.height + (1.0 - transitionFraction) * expandedFrame.height)) + controlsClippingFrame = CGRect(origin: CGPoint(x: transitionFraction * trueAvatarFrame.minX + (1.0 - transitionFraction) * expandedFrame.minX, y: transitionFraction * trueAvatarFrame.minY + (1.0 - transitionFraction) * expandedFrame.minY), size: CGSize(width: transitionFraction * trueAvatarFrame.width + (1.0 - transitionFraction) * expandedFrame.width, height: transitionFraction * trueAvatarFrame.height + (1.0 - transitionFraction) * expandedFrame.height)) } else { controlsClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: expandedAvatarListSize) } } else { - apparentAvatarFrame = CGRect(origin: CGPoint(x: avatarCenter.x - avatarFrame.width / 2.0, y: -contentOffset + avatarOffset + avatarCenter.y - avatarFrame.height / 2.0), size: avatarFrame.size) + var trueAvatarSize = avatarFrame.size + if self.avatarListNode.avatarContainerNode.avatarNode.storyStats != nil { + trueAvatarSize.width -= 3.0 * 4.0 + trueAvatarSize.height -= 3.0 * 4.0 + } + apparentAvatarFrame = CGRect(origin: CGPoint(x: avatarCenter.x - trueAvatarSize.width / 2.0, y: -contentOffset + avatarOffset + avatarCenter.y - trueAvatarSize.height / 2.0), size: trueAvatarSize) controlsClippingFrame = apparentAvatarFrame } @@ -3466,7 +3482,13 @@ final class PeerInfoHeaderNode: ASDisplayNode { if self.isAvatarExpanded { if let transitionSourceAvatarFrame = transitionSourceAvatarFrame { let neutralAvatarListContainerSize = expandedAvatarListSize - let avatarListContainerSize = CGSize(width: neutralAvatarListContainerSize.width * (1.0 - transitionFraction) + transitionSourceAvatarFrame.width * transitionFraction, height: neutralAvatarListContainerSize.height * (1.0 - transitionFraction) + transitionSourceAvatarFrame.height * transitionFraction) + var avatarListContainerSize = CGSize(width: neutralAvatarListContainerSize.width * (1.0 - transitionFraction) + transitionSourceAvatarFrame.width * transitionFraction, height: neutralAvatarListContainerSize.height * (1.0 - transitionFraction) + transitionSourceAvatarFrame.height * transitionFraction) + + if self.avatarListNode.avatarContainerNode.avatarNode.storyStats != nil { + avatarListContainerSize.width -= 1.33 * 5.0 + avatarListContainerSize.height -= 1.33 * 5.0 + } + avatarListContainerFrame = CGRect(origin: CGPoint(x: -avatarListContainerSize.width / 2.0, y: -avatarListContainerSize.height / 2.0), size: avatarListContainerSize) } else { avatarListContainerFrame = CGRect(origin: CGPoint(x: -expandedAvatarListSize.width / 2.0, y: -expandedAvatarListSize.height / 2.0), size: expandedAvatarListSize) @@ -3512,6 +3534,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { } self.avatarListNode.topCoverNode.update(maskValue) self.avatarListNode.maskNode.update(maskValue) + self.avatarListNode.bottomCoverNode.backgroundColor = UIColor(white: 0.0, alpha: maskValue) self.avatarListNode.listContainerNode.topShadowNode.isHidden = !self.isAvatarExpanded @@ -3520,7 +3543,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { avatarMaskOffset -= contentOffset } - self.avatarListNode.maskNode.position = CGPoint(x: 0.0, y: -self.avatarListNode.frame.minY + 48.0 + 85.5 + avatarMaskOffset) + self.avatarListNode.maskNode.position = CGPoint(x: 0.0, y: -self.avatarListNode.frame.minY + 48.0 + 85.0 + avatarMaskOffset) self.avatarListNode.maskNode.bounds = CGRect(origin: .zero, size: CGSize(width: 171.0, height: 171.0)) self.avatarListNode.bottomCoverNode.position = self.avatarListNode.maskNode.position diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoMembers.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoMembers.swift index df3d35aca6..a784f89437 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoMembers.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoMembers.swift @@ -12,15 +12,15 @@ enum PeerInfoMemberRole { } enum PeerInfoMember: Equatable { - case channelMember(RenderedChannelParticipant) - case legacyGroupMember(peer: RenderedPeer, role: PeerInfoMemberRole, invitedBy: PeerId?, presence: TelegramUserPresence?) + case channelMember(participant: RenderedChannelParticipant, storyStats: PeerStoryStats?) + case legacyGroupMember(peer: RenderedPeer, role: PeerInfoMemberRole, invitedBy: PeerId?, presence: TelegramUserPresence?, storyStats: PeerStoryStats?) case account(peer: RenderedPeer) var id: PeerId { switch self { - case let .channelMember(channelMember): - return channelMember.peer.id - case let .legacyGroupMember(peer, _, _, _): + case let .channelMember(participant, _): + return participant.peer.id + case let .legacyGroupMember(peer, _, _, _, _): return peer.peerId case let .account(peer): return peer.peerId @@ -29,9 +29,9 @@ enum PeerInfoMember: Equatable { var peer: Peer { switch self { - case let .channelMember(channelMember): - return channelMember.peer - case let .legacyGroupMember(peer, _, _, _): + case let .channelMember(participant, _): + return participant.peer + case let .legacyGroupMember(peer, _, _, _, _): return peer.peers[peer.peerId]! case let .account(peer): return peer.peers[peer.peerId]! @@ -40,9 +40,9 @@ enum PeerInfoMember: Equatable { var presence: TelegramUserPresence? { switch self { - case let .channelMember(channelMember): - return channelMember.presences[channelMember.peer.id] as? TelegramUserPresence - case let .legacyGroupMember(_, _, _, presence): + case let .channelMember(participant, _): + return participant.presences[participant.peer.id] as? TelegramUserPresence + case let .legacyGroupMember(_, _, _, presence, _): return presence case .account: return nil @@ -51,8 +51,8 @@ enum PeerInfoMember: Equatable { var role: PeerInfoMemberRole { switch self { - case let .channelMember(channelMember): - switch channelMember.participant { + case let .channelMember(participant, _): + switch participant.participant { case .creator: return .creator case let .member(_, _, adminInfo, _, _): @@ -62,7 +62,7 @@ enum PeerInfoMember: Equatable { return .member } } - case let .legacyGroupMember(_, role, _, _): + case let .legacyGroupMember(_, role, _, _, _): return role case .account: return .member @@ -71,8 +71,8 @@ enum PeerInfoMember: Equatable { var rank: String? { switch self { - case let .channelMember(channelMember): - switch channelMember.participant { + case let .channelMember(participant, _): + switch participant.participant { case let .creator(_, _, rank): return rank case let .member(_, _, _, _, rank): @@ -84,6 +84,17 @@ enum PeerInfoMember: Equatable { return nil } } + + var storyStats: PeerStoryStats? { + switch self { + case let .channelMember(_, value): + return value + case let .legacyGroupMember(_, _, _, _, value): + return value + case .account: + return nil + } + } } enum PeerInfoMembersDataState: Equatable { @@ -154,7 +165,9 @@ private final class PeerInfoMembersContextImpl { guard let strongSelf = self else { return } - let unsortedMembers = state.list.map(PeerInfoMember.channelMember) + let unsortedMembers = state.list.map { item -> PeerInfoMember in + return .channelMember(participant: item, storyStats: state.peerStoryStats[item.peer.id]) + } let members: [PeerInfoMember] if unsortedMembers.count <= 50 { members = membersSortedByPresence(unsortedMembers, accountPeerId: strongSelf.context.account.peerId) @@ -230,7 +243,7 @@ private final class PeerInfoMembersContextImpl { role = .member invitedBy = invitedByValue } - unsortedMembers.append(.legacyGroupMember(peer: RenderedPeer(peer: peer), role: role, invitedBy: invitedBy, presence: view.peerPresences[participant.peerId] as? TelegramUserPresence)) + unsortedMembers.append(.legacyGroupMember(peer: RenderedPeer(peer: peer), role: role, invitedBy: invitedBy, presence: view.peerPresences[participant.peerId] as? TelegramUserPresence, storyStats: view.memberStoryStats[participant.peerId])) } } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 4e945c7d28..e3386052d3 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -448,6 +448,7 @@ private enum PeerInfoMemberAction { case promote case restrict case remove + case openStories(sourceView: UIView) } private enum PeerInfoContextSubject { @@ -1389,6 +1390,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese case .remove: interaction.performMemberAction(member, .remove) } + }, openStories: { sourceView in + interaction.performMemberAction(member, .openStories(sourceView: sourceView)) })) } } @@ -3012,6 +3015,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro strongSelf.performMemberAction(member: member, action: .restrict) case .remove: strongSelf.performMemberAction(member: member, action: .remove) + case let .openStories(sourceView): + strongSelf.performMemberAction(member: member, action: .openStories(sourceView: sourceView)) } } @@ -7099,7 +7104,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } switch action { case .promote: - if case let .channelMember(channelMember) = member { + if case let .channelMember(channelMember, _) = member { var upgradedToSupergroupImpl: (() -> Void)? let controller = channelAdminController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id, adminId: member.id, initialParticipant: channelMember.participant, updated: { _ in }, upgradedToSupergroup: { _, f in @@ -7116,7 +7121,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } case .restrict: - if case let .channelMember(channelMember) = member { + if case let .channelMember(channelMember, _) = member { var upgradedToSupergroupImpl: (() -> Void)? let controller = channelBannedMemberController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id, memberId: member.id, initialParticipant: channelMember.participant, updated: { _ in @@ -7135,6 +7140,71 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } case .remove: data.members?.membersContext.removeMember(memberId: member.id) + case let .openStories(sourceView): + let storyContent = StoryContentContextImpl(context: self.context, isHidden: false, focusedPeerId: member.id, singlePeer: true) + let _ = (storyContent.state + |> filter { $0.slice != nil } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self, weak sourceView] _ in + guard let self else { + return + } + + var transitionIn: StoryContainerScreen.TransitionIn? + if let sourceView { + transitionIn = StoryContainerScreen.TransitionIn( + sourceView: sourceView, + sourceRect: sourceView.bounds, + sourceCornerRadius: sourceView.bounds.width * 0.5, + sourceIsAvatar: false + ) + sourceView.isHidden = true + } + + let storyContainerScreen = StoryContainerScreen( + context: self.context, + content: storyContent, + transitionIn: transitionIn, + transitionOut: { peerId, _ in + if let sourceView { + let destinationView = sourceView + return StoryContainerScreen.TransitionOut( + destinationView: destinationView, + transitionView: StoryContainerScreen.TransitionView( + makeView: { [weak destinationView] in + let parentView = UIView() + if let copyView = destinationView?.snapshotContentTree(unhide: true) { + parentView.addSubview(copyView) + } + return parentView + }, + updateView: { copyView, state, transition in + guard let view = copyView.subviews.first else { + return + } + let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress) + transition.setPosition(view: view, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) + transition.setScale(view: view, scale: size.width / state.destinationSize.width) + }, + insertCloneTransitionView: nil + ), + destinationRect: destinationView.bounds, + destinationCornerRadius: destinationView.bounds.width * 0.5, + destinationIsAvatar: false, + completed: { [weak sourceView] in + guard let sourceView else { + return + } + sourceView.isHidden = false + } + ) + } else { + return nil + } + } + ) + self.controller?.push(storyContainerScreen) + }) } } @@ -10503,6 +10573,8 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig private var previousTitleNode: (ASDisplayNode, PortalView)? private var previousStatusNode: (ASDisplayNode, ASDisplayNode)? + private var previousAvatarView: UIView? + private var didSetup: Bool = false init(screenNode: PeerInfoScreenNode, presentationData: PresentationData, headerNode: PeerInfoHeaderNode) { @@ -10576,7 +10648,9 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig self.view.layer.addSublayer(previousRightButton) } } else { - if let _ = bottomNavigationBar.rightButtonNode.singleCustomNode as? ChatAvatarNavigationNode { + if let avatarNavigationNode = bottomNavigationBar.rightButtonNode.singleCustomNode as? ChatAvatarNavigationNode, let previousAvatarView = avatarNavigationNode.view.snapshotContentTree() { + self.previousAvatarView = previousAvatarView + self.view.addSubview(previousAvatarView) } else if let previousRightButton = bottomNavigationBar.rightButtonNode.view.layer.snapshotContentTree() { self.previousRightButton = previousRightButton self.view.layer.addSublayer(previousRightButton) @@ -10706,7 +10780,7 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig let previousTitleFrame = previousTitleView.titleContainerView.convert(previousTitleView.titleContainerView.bounds, to: bottomNavigationBar.view) let previousStatusFrame = previousTitleView.activityNode.view.convert(previousTitleView.activityNode.bounds, to: bottomNavigationBar.view) - self.headerNode.navigationTransition = PeerInfoHeaderNavigationTransition(sourceNavigationBar: bottomNavigationBar, sourceTitleView: previousTitleView, sourceTitleFrame: previousTitleFrame, sourceSubtitleFrame: previousStatusFrame, fraction: fraction) + self.headerNode.navigationTransition = PeerInfoHeaderNavigationTransition(sourceNavigationBar: bottomNavigationBar, sourceTitleView: previousTitleView, sourceTitleFrame: previousTitleFrame, sourceSubtitleFrame: previousStatusFrame, previousAvatarView: self.previousAvatarView, fraction: fraction) var topHeight = topNavigationBar.backgroundNode.bounds.height if let iconView = previousTitleView.titleCredibilityIconView.componentView { diff --git a/submodules/TemporaryCachedPeerDataManager/Sources/ChannelMemberCategoryListContext.swift b/submodules/TemporaryCachedPeerDataManager/Sources/ChannelMemberCategoryListContext.swift index fb879ae51f..c68e194539 100644 --- a/submodules/TemporaryCachedPeerDataManager/Sources/ChannelMemberCategoryListContext.swift +++ b/submodules/TemporaryCachedPeerDataManager/Sources/ChannelMemberCategoryListContext.swift @@ -48,16 +48,9 @@ public extension ChannelParticipant { } public struct ChannelMemberListState { - public let list: [RenderedChannelParticipant] - public let loadingState: ChannelMemberListLoadingState - - public func withUpdatedList(_ list: [RenderedChannelParticipant]) -> ChannelMemberListState { - return ChannelMemberListState(list: list, loadingState: self.loadingState) - } - - public func withUpdatedLoadingState(_ loadingState: ChannelMemberListLoadingState) -> ChannelMemberListState { - return ChannelMemberListState(list: self.list, loadingState: loadingState) - } + public var list: [RenderedChannelParticipant] + public var peerStoryStats: [EnginePeer.Id: PeerStoryStats] + public var loadingState: ChannelMemberListLoadingState } enum ChannelMemberListCategory { @@ -72,7 +65,7 @@ enum ChannelMemberListCategory { } private protocol ChannelMemberCategoryListContext { - var listStateValue: ChannelMemberListState { get } + //var listStateValue: ChannelMemberListState { get } var listState: Signal { get } func loadMore() func reset(_ force: Bool) @@ -139,7 +132,20 @@ private final class ChannelMemberSingleCategoryListContext: ChannelMemberCategor } private var listStatePromise: Promise var listState: Signal { + let postbox = self.postbox return self.listStatePromise.get() + |> mapToSignal { state -> Signal in + let key: PostboxViewKey = .peerStoryStats(peerIds: Set(state.list.map(\.peer.id))) + return postbox.combinedView(keys: [key]) + |> map { views -> ChannelMemberListState in + var state = state + if let view = views.views[key] as? PeerStoryStatsView { + state.peerStoryStats = view.storyStats + } + + return state + } + } } private let loadingDisposable = MetaDisposable() @@ -155,7 +161,7 @@ private final class ChannelMemberSingleCategoryListContext: ChannelMemberCategor self.peerId = peerId self.category = category - self.listStateValue = ChannelMemberListState(list: [], loadingState: .ready(hasMore: true)) + self.listStateValue = ChannelMemberListState(list: [], peerStoryStats: [:], loadingState: .ready(hasMore: true)) self.listStatePromise = Promise(self.listStateValue) self.loadMoreInternal(initial: true) } @@ -182,7 +188,7 @@ private final class ChannelMemberSingleCategoryListContext: ChannelMemberCategor loadCount = requestBatchSize } - self.listStateValue = self.listStateValue.withUpdatedLoadingState(.loading(initial: initial)) + self.listStateValue.loadingState = .loading(initial: initial) self.loadingDisposable.set((self.loadMoreSignal(count: loadCount) |> deliverOnMainQueue).start(next: { [weak self] members in @@ -201,7 +207,10 @@ private final class ChannelMemberSingleCategoryListContext: ChannelMemberCategor } self.loadingDisposable.set(nil) - self.listStateValue = self.listStateValue.withUpdatedLoadingState(loadingState).withUpdatedList(list) + var listState = self.listStateValue + listState.loadingState = loadingState + listState.list = list + self.listStateValue = listState } } @@ -264,7 +273,11 @@ private final class ChannelMemberSingleCategoryListContext: ChannelMemberCategor } } self.loadingDisposable.set(nil) - self.listStateValue = self.listStateValue.withUpdatedList(list) + + var listState = self.listStateValue + listState.list = list + self.listStateValue = listState + if case .loading = self.listStateValue.loadingState { self.loadMore() } @@ -290,7 +303,12 @@ private final class ChannelMemberSingleCategoryListContext: ChannelMemberCategor list.append(member) } } - self.listStateValue = self.listStateValue.withUpdatedList(list).withUpdatedLoadingState(.ready(hasMore: members.count >= requestBatchSize)) + + var listState = self.listStateValue + listState.loadingState = .ready(hasMore: members.count >= requestBatchSize) + listState.list = list + self.listStateValue = listState + if firstLoad { self.checkUpdateHead() } @@ -534,7 +552,9 @@ private final class ChannelMemberSingleCategoryListContext: ChannelMemberCategor } } if updatedList { - self.listStateValue = self.listStateValue.withUpdatedList(list) + var listState = self.listStateValue + listState.list = list + self.listStateValue = listState } } } @@ -542,9 +562,9 @@ private final class ChannelMemberSingleCategoryListContext: ChannelMemberCategor private final class ChannelMemberMultiCategoryListContext: ChannelMemberCategoryListContext { private var contexts: [ChannelMemberSingleCategoryListContext] = [] - var listStateValue: ChannelMemberListState { + /*var listStateValue: ChannelMemberListState { return ChannelMemberMultiCategoryListContext.reduceListStates(self.contexts.map { $0.listStateValue }) - } + }*/ private static func reduceListStates(_ listStates: [ChannelMemberListState]) -> ChannelMemberListState { var allReady = true @@ -555,12 +575,13 @@ private final class ChannelMemberMultiCategoryListContext: ChannelMemberCategory } } if !allReady { - return ChannelMemberListState(list: [], loadingState: .loading(initial: true)) + return ChannelMemberListState(list: [], peerStoryStats: [:], loadingState: .loading(initial: true)) } var list: [RenderedChannelParticipant] = [] var existingIds = Set() var loadingState: ChannelMemberListLoadingState = .ready(hasMore: false) + var peerStoryStats: [PeerId: PeerStoryStats] = [:] loop: for i in 0 ..< listStates.count { for item in listStates[i].list { if !existingIds.contains(item.peer.id) { @@ -568,18 +589,21 @@ private final class ChannelMemberMultiCategoryListContext: ChannelMemberCategory list.append(item) } } + for (id, value) in listStates[i].peerStoryStats { + peerStoryStats[id] = value + } switch listStates[i].loadingState { - case let .loading(initial): - loadingState = .loading(initial: initial) + case let .loading(initial): + loadingState = .loading(initial: initial) + break loop + case let .ready(hasMore): + if hasMore { + loadingState = .ready(hasMore: true) break loop - case let .ready(hasMore): - if hasMore { - loadingState = .ready(hasMore: true) - break loop - } + } } } - return ChannelMemberListState(list: list, loadingState: loadingState) + return ChannelMemberListState(list: list, peerStoryStats: peerStoryStats, loadingState: loadingState) } var listState: Signal { @@ -639,6 +663,7 @@ private final class PeerChannelMemberContextWithSubscribers { private let subscribers = Bag<(ChannelMemberListState) -> Void>() private let disposable = MetaDisposable() private let becameEmpty: () -> Void + private var currentValue: ChannelMemberListState? private var emptyTimer: SwiftSignalKit.Timer? @@ -649,6 +674,7 @@ private final class PeerChannelMemberContextWithSubscribers { self.disposable.set((context.listState |> deliverOnMainQueue).start(next: { [weak self] value in if let strongSelf = self { + strongSelf.currentValue = value for f in strongSelf.subscribers.copyItems() { f(value) } @@ -678,7 +704,9 @@ private final class PeerChannelMemberContextWithSubscribers { func subscribe(requestUpdate: Bool, updated: @escaping (ChannelMemberListState) -> Void) -> Disposable { let wasEmpty = self.subscribers.isEmpty let index = self.subscribers.add(updated) - updated(self.context.listStateValue) + if let currentValue = self.currentValue { + updated(currentValue) + } if wasEmpty { self.emptyTimer?.invalidate() if requestUpdate { diff --git a/submodules/TooltipUI/Sources/TooltipScreen.swift b/submodules/TooltipUI/Sources/TooltipScreen.swift index ac65cd1638..925a7fd833 100644 --- a/submodules/TooltipUI/Sources/TooltipScreen.swift +++ b/submodules/TooltipUI/Sources/TooltipScreen.swift @@ -707,7 +707,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { component: AnyComponent(AvatarStoryIndicatorComponent( hasUnseen: true, hasUnseenCloseFriendsItems: false, - theme: defaultDarkPresentationTheme, + colors: AvatarStoryIndicatorComponent.Colors(theme: defaultDarkPresentationTheme), activeLineWidth: 1.0 + UIScreenPixel, inactiveLineWidth: 1.0 + UIScreenPixel, counters: nil