This commit is contained in:
Ali 2023-07-09 02:07:53 +04:00
parent 2575b0e2f1
commit 008b52a250
26 changed files with 833 additions and 218 deletions

View File

@ -665,8 +665,8 @@ public final class AvatarNode: ASDisplayNode {
} }
public let contentNode: ContentNode public let contentNode: ContentNode
private var storyIndicatorTheme: PresentationTheme?
private var storyIndicator: ComponentView<Empty>? private var storyIndicator: ComponentView<Empty>?
public private(set) var storyPresentationParams: StoryPresentationParams?
public struct StoryStats: Equatable { public struct StoryStats: Equatable {
public var totalCount: Int public var totalCount: Int
@ -735,9 +735,7 @@ public final class AvatarNode: ASDisplayNode {
guard let self else { guard let self else {
return return
} }
if let storyIndicatorTheme = self.storyIndicatorTheme { self.updateStoryIndicator(transition: .immediate)
self.updateStoryIndicator(theme: storyIndicatorTheme, transition: .immediate)
}
} }
self.addSubnode(self.contentNode) self.addSubnode(self.contentNode)
@ -766,9 +764,7 @@ public final class AvatarNode: ASDisplayNode {
self.contentNode.updateSize(size: size) self.contentNode.updateSize(size: size)
if let storyIndicatorTheme = self.storyIndicatorTheme { self.updateStoryIndicator(transition: .immediate)
self.updateStoryIndicator(theme: storyIndicatorTheme, transition: .immediate)
}
} }
public func playArchiveAnimation() { public func playArchiveAnimation() {
@ -807,51 +803,71 @@ public final class AvatarNode: ASDisplayNode {
self.contentNode.setCustomLetters(letters, explicitColor: explicitColor, icon: icon) self.contentNode.setCustomLetters(letters, explicitColor: explicitColor, icon: icon)
} }
public func setStoryStats(storyStats: StoryStats?, theme: PresentationTheme, transition: Transition) { public func setStoryStats(storyStats: StoryStats?, presentationParams: StoryPresentationParams, transition: Transition) {
if self.storyStats != storyStats || self.storyIndicatorTheme !== theme { if self.storyStats != storyStats || self.storyPresentationParams != presentationParams {
self.storyStats = storyStats self.storyStats = storyStats
self.storyIndicatorTheme = theme self.storyPresentationParams = presentationParams
self.updateStoryIndicator(theme: theme, transition: transition) self.updateStoryIndicator(transition: transition)
} }
} }
private struct StoryIndicatorParams { public struct Colors: Equatable {
let lineWidth: CGFloat public var unseenColors: [UIColor]
let indicatorSize: CGSize public var unseenCloseFriendsColors: [UIColor]
let avatarScale: CGFloat 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.lineWidth = lineWidth
self.indicatorSize = indicatorSize self.inactiveLineWidth = inactiveLineWidth
self.avatarScale = avatarScale
} }
} }
private func storyIndicatorParams(size: CGSize) -> StoryIndicatorParams { private func updateStoryIndicator(transition: Transition) {
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) {
if !self.isNodeLoaded { if !self.isNodeLoaded {
return return
} }
if self.bounds.isEmpty { if self.bounds.isEmpty {
return return
} }
guard let storyPresentationParams = self.storyPresentationParams else {
self.storyIndicatorTheme = theme return
}
let size = self.bounds.size let size = self.bounds.size
if let storyStats = self.storyStats { 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<Empty> let storyIndicator: ComponentView<Empty>
var indicatorTransition = transition var indicatorTransition = transition
@ -867,25 +883,28 @@ public final class AvatarNode: ASDisplayNode {
component: AnyComponent(AvatarStoryIndicatorComponent( component: AnyComponent(AvatarStoryIndicatorComponent(
hasUnseen: storyStats.unseenCount != 0, hasUnseen: storyStats.unseenCount != 0,
hasUnseenCloseFriendsItems: storyStats.hasUnseenCloseFriendsItems, hasUnseenCloseFriendsItems: storyStats.hasUnseenCloseFriendsItems,
theme: theme, colors: AvatarStoryIndicatorComponent.Colors(
activeLineWidth: indicatorParams.lineWidth, unseenColors: storyPresentationParams.colors.unseenColors,
inactiveLineWidth: indicatorParams.lineWidth, unseenCloseFriendsColors: storyPresentationParams.colors.unseenCloseFriendsColors,
isGlassBackground: false, seenColors: storyPresentationParams.colors.seenColors
),
activeLineWidth: activeLineWidth,
inactiveLineWidth: inactiveLineWidth,
counters: AvatarStoryIndicatorComponent.Counters( counters: AvatarStoryIndicatorComponent.Counters(
totalCount: storyStats.totalCount, totalCount: storyStats.totalCount,
unseenCount: storyStats.unseenCount unseenCount: storyStats.unseenCount
) )
)), )),
environment: {}, environment: {},
containerSize: indicatorParams.indicatorSize containerSize: indicatorSize
) )
if let storyIndicatorView = storyIndicator.view { if let storyIndicatorView = storyIndicator.view {
if storyIndicatorView.superview == nil { if storyIndicatorView.superview == nil {
self.view.addSubview(storyIndicatorView) 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 { } else {
transition.setScale(view: self.contentNode.view, scale: 1.0) transition.setScale(view: self.contentNode.view, scale: 1.0)
if let storyIndicator = self.storyIndicator { if let storyIndicator = self.storyIndicator {

View File

@ -2841,7 +2841,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
component: AnyComponent(AvatarStoryIndicatorComponent( component: AnyComponent(AvatarStoryIndicatorComponent(
hasUnseen: storyState.stats.unseenCount != 0, hasUnseen: storyState.stats.unseenCount != 0,
hasUnseenCloseFriendsItems: storyState.hasUnseenCloseFriends, hasUnseenCloseFriendsItems: storyState.hasUnseenCloseFriends,
theme: item.presentationData.theme, colors: AvatarStoryIndicatorComponent.Colors(theme: item.presentationData.theme),
activeLineWidth: 2.33, activeLineWidth: 2.33,
inactiveLineWidth: 1.33, inactiveLineWidth: 1.33,
counters: AvatarStoryIndicatorComponent.Counters( counters: AvatarStoryIndicatorComponent.Counters(

View File

@ -1115,7 +1115,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
component: AnyComponent(AvatarStoryIndicatorComponent( component: AnyComponent(AvatarStoryIndicatorComponent(
hasUnseen: storyStats.unseen != 0, hasUnseen: storyStats.unseen != 0,
hasUnseenCloseFriendsItems: storyStats.hasUnseenCloseFriends, hasUnseenCloseFriendsItems: storyStats.hasUnseenCloseFriends,
theme: item.presentationData.theme, colors: AvatarStoryIndicatorComponent.Colors(theme: item.presentationData.theme),
activeLineWidth: 1.0 + UIScreenPixel, activeLineWidth: 1.0 + UIScreenPixel,
inactiveLineWidth: 1.0 + UIScreenPixel, inactiveLineWidth: 1.0 + UIScreenPixel,
counters: AvatarStoryIndicatorComponent.Counters(totalCount: storyStats.total, unseenCount: storyStats.unseen) counters: AvatarStoryIndicatorComponent.Counters(totalCount: storyStats.total, unseenCount: storyStats.unseen)

View File

@ -355,8 +355,10 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem {
let shimmering: ItemListPeerItemShimmering? let shimmering: ItemListPeerItemShimmering?
let displayDecorations: Bool let displayDecorations: Bool
let disableInteractiveTransitionIfNecessary: 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.presentationData = presentationData
self.dateTimeFormat = dateTimeFormat self.dateTimeFormat = dateTimeFormat
self.nameDisplayOrder = nameDisplayOrder self.nameDisplayOrder = nameDisplayOrder
@ -393,6 +395,8 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem {
self.shimmering = shimmering self.shimmering = shimmering
self.displayDecorations = displayDecorations self.displayDecorations = displayDecorations
self.disableInteractiveTransitionIfNecessary = disableInteractiveTransitionIfNecessary 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<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) { public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
@ -471,6 +475,8 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
private var avatarIconComponent: EmojiStatusComponent? private var avatarIconComponent: EmojiStatusComponent?
private var avatarIconView: ComponentView<Empty>? private var avatarIconView: ComponentView<Empty>?
private var avatarButton: HighlightTrackingButton?
private let titleNode: TextNode private let titleNode: TextNode
private let labelNode: TextNode private let labelNode: TextNode
private let labelBadgeNode: ASImageNode 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)) 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) 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 { if let switchValue = item.switchValue, case .leftCheck = switchValue.style {
let leftCheckNode: CheckNode let leftCheckNode: CheckNode
if let current = strongSelf.leftCheckNode { if let current = strongSelf.leftCheckNode {
@ -1332,6 +1354,17 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
clipStyle = .roundedRect 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.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)
} }
} }
@ -1444,10 +1477,14 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated) super.setHighlighted(highlighted, at: point, animated: animated)
if let avatarButton = self.avatarButton, avatarButton.bounds.contains(self.view.convert(point, to: avatarButton)) {
self.isHighlighted = false
} else {
self.isHighlighted = highlighted 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) { override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
@ -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.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 { if let avatarIconComponentView = self.avatarIconView?.view {
let avatarFrame = self.avatarNode.frame let avatarFrame = self.avatarNode.frame
@ -1580,6 +1621,13 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
} }
return false return false
} }
@objc private func avatarButtonPressed() {
guard let item = self.layoutParams?.0 else {
return
}
item.openStories?(self.avatarNode.view)
}
} }
public final class ItemListPeerItemHeader: ListViewItemHeader { public final class ItemListPeerItemHeader: ListViewItemHeader {

View File

@ -0,0 +1,76 @@
import Foundation
final class MutablePeerStoryStatsView: MutablePostboxView {
let peerIds: Set<PeerId>
var storyStats: [PeerId: PeerStoryStats] = [:]
init(postbox: PostboxImpl, peerIds: Set<PeerId>) {
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<PeerId>()
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
}
}

View File

@ -11,8 +11,9 @@ public struct PeerViewComponents: OptionSet {
public static let subPeers = PeerViewComponents(rawValue: 1 << 1) public static let subPeers = PeerViewComponents(rawValue: 1 << 1)
public static let messages = PeerViewComponents(rawValue: 1 << 2) public static let messages = PeerViewComponents(rawValue: 1 << 2)
public static let groupId = PeerViewComponents(rawValue: 1 << 3) 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 { final class MutablePeerView: MutablePostboxView {
@ -27,6 +28,8 @@ final class MutablePeerView: MutablePostboxView {
var media: [MediaId: Media] = [:] var media: [MediaId: Media] = [:]
var peerIsContact: Bool var peerIsContact: Bool
var groupId: PeerGroupId? var groupId: PeerGroupId?
var storyStats: PeerStoryStats?
var memberStoryStats: [PeerId: PeerStoryStats] = [:]
init(postbox: PostboxImpl, peerId: PeerId, components: PeerViewComponents) { init(postbox: PostboxImpl, peerId: PeerId, components: PeerViewComponents) {
self.components = components self.components = components
@ -54,8 +57,10 @@ final class MutablePeerView: MutablePostboxView {
} }
self.cachedData = postbox.cachedPeerDataTable.get(contactPeerId) self.cachedData = postbox.cachedPeerDataTable.get(contactPeerId)
self.peerIsContact = postbox.contactsTable.isContact(peerId: self.contactPeerId) self.peerIsContact = postbox.contactsTable.isContact(peerId: self.contactPeerId)
var cachedDataPeerIds = Set<PeerId>()
if let cachedData = self.cachedData { if let cachedData = self.cachedData {
peerIds.formUnion(cachedData.peerIds) cachedDataPeerIds = cachedData.peerIds
peerIds.formUnion(cachedDataPeerIds)
messageIds.formUnion(cachedData.messageIds) messageIds.formUnion(cachedData.messageIds)
} }
for id in peerIds { for id in peerIds {
@ -66,6 +71,11 @@ final class MutablePeerView: MutablePostboxView {
self.peerPresences[id] = presence 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 = self.peers[peerId], let associatedPeerId = peer.associatedPeerId {
if let peer = getPeer(associatedPeerId) { if let peer = getPeer(associatedPeerId) {
self.peers[associatedPeerId] = peer self.peers[associatedPeerId] = peer
@ -83,6 +93,10 @@ final class MutablePeerView: MutablePostboxView {
} }
} }
self.media = renderAssociatedMediaForPeers(postbox: postbox, peers: self.peers) 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 { 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<PeerId>()
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 return updated
} }
@ -282,6 +343,8 @@ public final class PeerView: PostboxView {
public let media: [MediaId: Media] public let media: [MediaId: Media]
public let peerIsContact: Bool public let peerIsContact: Bool
public let groupId: PeerGroupId? public let groupId: PeerGroupId?
public let storyStats: PeerStoryStats?
public let memberStoryStats: [PeerId: PeerStoryStats]
init(_ mutableView: MutablePeerView) { init(_ mutableView: MutablePeerView) {
self.peerId = mutableView.peerId self.peerId = mutableView.peerId
@ -293,5 +356,7 @@ public final class PeerView: PostboxView {
self.media = mutableView.media self.media = mutableView.media
self.peerIsContact = mutableView.peerIsContact self.peerIsContact = mutableView.peerIsContact
self.groupId = mutableView.groupId self.groupId = mutableView.groupId
self.storyStats = mutableView.storyStats
self.memberStoryStats = mutableView.memberStoryStats
} }
} }

View File

@ -1327,6 +1327,10 @@ public final class Transaction {
public func getExpiredStoryIds(belowTimestamp: Int32) -> [StoryId] { public func getExpiredStoryIds(belowTimestamp: Int32) -> [StoryId] {
return self.postbox!.storyItemsTable.getExpiredIds(belowTimestamp: belowTimestamp) return self.postbox!.storyItemsTable.getExpiredIds(belowTimestamp: belowTimestamp)
} }
public func getPeerStoryStats(peerId: PeerId) -> PeerStoryStats? {
return fetchPeerStoryStats(postbox: self.postbox!, peerId: peerId)
}
} }
public enum PostboxResult { public enum PostboxResult {

View File

@ -44,6 +44,7 @@ public enum PostboxViewKey: Hashable {
case storiesState(key: PostboxStoryStatesKey) case storiesState(key: PostboxStoryStatesKey)
case storyItems(peerId: PeerId) case storyItems(peerId: PeerId)
case storyExpirationTimeItems case storyExpirationTimeItems
case peerStoryStats(peerIds: Set<PeerId>)
public func hash(into hasher: inout Hasher) { public func hash(into hasher: inout Hasher) {
switch self { switch self {
@ -147,6 +148,8 @@ public enum PostboxViewKey: Hashable {
hasher.combine(peerId) hasher.combine(peerId)
case .storyExpirationTimeItems: case .storyExpirationTimeItems:
hasher.combine(19) hasher.combine(19)
case let .peerStoryStats(peerIds):
hasher.combine(peerIds)
} }
} }
@ -410,6 +413,12 @@ public enum PostboxViewKey: Hashable {
} else { } else {
return false 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) return MutableStoryItemsView(postbox: postbox, peerId: peerId)
case .storyExpirationTimeItems: case .storyExpirationTimeItems:
return MutableStoryExpirationTimeItemsView(postbox: postbox) return MutableStoryExpirationTimeItemsView(postbox: postbox)
case let .peerStoryStats(peerIds):
return MutablePeerStoryStatsView(postbox: postbox, peerIds: peerIds)
} }
} }

View File

@ -1490,13 +1490,16 @@ public final class EngineStoryViewListContext {
public final class Item: Equatable { public final class Item: Equatable {
public let peer: EnginePeer public let peer: EnginePeer
public let timestamp: Int32 public let timestamp: Int32
public let storyStats: PeerStoryStats?
public init( public init(
peer: EnginePeer, peer: EnginePeer,
timestamp: Int32 timestamp: Int32,
storyStats: PeerStoryStats?
) { ) {
self.peer = peer self.peer = peer
self.timestamp = timestamp self.timestamp = timestamp
self.storyStats = storyStats
} }
public static func ==(lhs: Item, rhs: Item) -> Bool { public static func ==(lhs: Item, rhs: Item) -> Bool {
@ -1506,6 +1509,9 @@ public final class EngineStoryViewListContext {
if lhs.timestamp != rhs.timestamp { if lhs.timestamp != rhs.timestamp {
return false return false
} }
if lhs.storyStats != rhs.storyStats {
return false
}
return true return true
} }
} }
@ -1545,6 +1551,7 @@ public final class EngineStoryViewListContext {
let storyId: Int32 let storyId: Int32
let disposable = MetaDisposable() let disposable = MetaDisposable()
let storyStatsDisposable = MetaDisposable()
var state: InternalState var state: InternalState
let statePromise = Promise<InternalState>() let statePromise = Promise<InternalState>()
@ -1569,6 +1576,7 @@ public final class EngineStoryViewListContext {
assert(self.queue.isCurrent()) assert(self.queue.isCurrent())
self.disposable.dispose() self.disposable.dispose()
self.storyStatsDisposable.dispose()
} }
func loadMore() { func loadMore() {
@ -1604,8 +1612,9 @@ public final class EngineStoryViewListContext {
for view in views { for view in views {
switch view { switch view {
case let .storyView(userId, date): case let .storyView(userId, date):
if let peer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))) { let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))
items.append(Item(peer: EnginePeer(peer), timestamp: date)) 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) nextOffset = NextOffset(id: userId, timestamp: date)
} }
@ -1702,6 +1711,35 @@ public final class EngineStoryViewListContext {
strongSelf.isLoadingMore = false strongSelf.isLoadingMore = false
strongSelf.statePromise.set(.single(strongSelf.state)) 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))
}
}))
})) }))
} }
} }

View File

@ -214,7 +214,7 @@ public final class ChatAvatarNavigationNode: ASDisplayNode {
component: AnyComponent(AvatarStoryIndicatorComponent( component: AnyComponent(AvatarStoryIndicatorComponent(
hasUnseen: storyData.hasUnseen, hasUnseen: storyData.hasUnseen,
hasUnseenCloseFriendsItems: storyData.hasUnseenCloseFriends, hasUnseenCloseFriendsItems: storyData.hasUnseenCloseFriends,
theme: theme, colors: AvatarStoryIndicatorComponent.Colors(theme: theme),
activeLineWidth: 1.0, activeLineWidth: 1.0,
inactiveLineWidth: 1.0, inactiveLineWidth: 1.0,
counters: nil counters: nil

View File

@ -5,6 +5,28 @@ import ComponentFlow
import TelegramPresentationData import TelegramPresentationData
public final class AvatarStoryIndicatorComponent: Component { 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 struct Counters: Equatable {
public var totalCount: Int public var totalCount: Int
public var unseenCount: Int public var unseenCount: Int
@ -17,27 +39,24 @@ public final class AvatarStoryIndicatorComponent: Component {
public let hasUnseen: Bool public let hasUnseen: Bool
public let hasUnseenCloseFriendsItems: Bool public let hasUnseenCloseFriendsItems: Bool
public let theme: PresentationTheme public let colors: Colors
public let activeLineWidth: CGFloat public let activeLineWidth: CGFloat
public let inactiveLineWidth: CGFloat public let inactiveLineWidth: CGFloat
public let isGlassBackground: Bool
public let counters: Counters? public let counters: Counters?
public init( public init(
hasUnseen: Bool, hasUnseen: Bool,
hasUnseenCloseFriendsItems: Bool, hasUnseenCloseFriendsItems: Bool,
theme: PresentationTheme, colors: Colors,
activeLineWidth: CGFloat, activeLineWidth: CGFloat,
inactiveLineWidth: CGFloat, inactiveLineWidth: CGFloat,
isGlassBackground: Bool = false,
counters: Counters? counters: Counters?
) { ) {
self.hasUnseen = hasUnseen self.hasUnseen = hasUnseen
self.hasUnseenCloseFriendsItems = hasUnseenCloseFriendsItems self.hasUnseenCloseFriendsItems = hasUnseenCloseFriendsItems
self.theme = theme self.colors = colors
self.activeLineWidth = activeLineWidth self.activeLineWidth = activeLineWidth
self.inactiveLineWidth = inactiveLineWidth self.inactiveLineWidth = inactiveLineWidth
self.isGlassBackground = isGlassBackground
self.counters = counters self.counters = counters
} }
@ -48,7 +67,7 @@ public final class AvatarStoryIndicatorComponent: Component {
if lhs.hasUnseenCloseFriendsItems != rhs.hasUnseenCloseFriendsItems { if lhs.hasUnseenCloseFriendsItems != rhs.hasUnseenCloseFriendsItems {
return false return false
} }
if lhs.theme !== rhs.theme { if lhs.colors != rhs.colors {
return false return false
} }
if lhs.activeLineWidth != rhs.activeLineWidth { if lhs.activeLineWidth != rhs.activeLineWidth {
@ -57,9 +76,6 @@ public final class AvatarStoryIndicatorComponent: Component {
if lhs.inactiveLineWidth != rhs.inactiveLineWidth { if lhs.inactiveLineWidth != rhs.inactiveLineWidth {
return false return false
} }
if lhs.isGlassBackground != rhs.isGlassBackground {
return false
}
if lhs.counters != rhs.counters { if lhs.counters != rhs.counters {
return false return false
} }
@ -101,16 +117,12 @@ public final class AvatarStoryIndicatorComponent: Component {
let inactiveColors: [CGColor] let inactiveColors: [CGColor]
if component.hasUnseenCloseFriendsItems { if component.hasUnseenCloseFriendsItems {
activeColors = [component.theme.chatList.storyUnseenPrivateColors.topColor.cgColor, component.theme.chatList.storyUnseenPrivateColors.bottomColor.cgColor] activeColors = component.colors.unseenCloseFriendsColors.map(\.cgColor)
} else { } else {
activeColors = [component.theme.chatList.storyUnseenColors.topColor.cgColor, component.theme.chatList.storyUnseenColors.bottomColor.cgColor] activeColors = component.colors.unseenColors.map(\.cgColor)
} }
if component.isGlassBackground { inactiveColors = component.colors.seenColors.map(\.cgColor)
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]
}
var locations: [CGFloat] = [0.0, 1.0] var locations: [CGFloat] = [0.0, 1.0]

View File

@ -17,6 +17,7 @@ swift_library(
"//submodules/SSignalKit/SwiftSignalKit", "//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AccountContext", "//submodules/AccountContext",
"//submodules/TelegramCore", "//submodules/TelegramCore",
"//submodules/Postbox",
"//submodules/Components/MultilineTextComponent", "//submodules/Components/MultilineTextComponent",
"//submodules/AvatarNode", "//submodules/AvatarNode",
"//submodules/CheckNode", "//submodules/CheckNode",

View File

@ -6,6 +6,7 @@ import ComponentFlow
import SwiftSignalKit import SwiftSignalKit
import AccountContext import AccountContext
import TelegramCore import TelegramCore
import Postbox
import MultilineTextComponent import MultilineTextComponent
import AvatarNode import AvatarNode
import TelegramPresentationData import TelegramPresentationData
@ -49,12 +50,14 @@ public final class PeerListItemComponent: Component {
let sideInset: CGFloat let sideInset: CGFloat
let title: String let title: String
let peer: EnginePeer? let peer: EnginePeer?
let storyStats: PeerStoryStats?
let subtitle: String? let subtitle: String?
let subtitleAccessory: SubtitleAccessory let subtitleAccessory: SubtitleAccessory
let presence: EnginePeer.Presence? let presence: EnginePeer.Presence?
let selectionState: SelectionState let selectionState: SelectionState
let hasNext: Bool let hasNext: Bool
let action: (EnginePeer) -> Void let action: (EnginePeer) -> Void
let openStories: ((EnginePeer, UIView) -> Void)?
public init( public init(
context: AccountContext, context: AccountContext,
@ -64,12 +67,14 @@ public final class PeerListItemComponent: Component {
sideInset: CGFloat, sideInset: CGFloat,
title: String, title: String,
peer: EnginePeer?, peer: EnginePeer?,
storyStats: PeerStoryStats? = nil,
subtitle: String?, subtitle: String?,
subtitleAccessory: SubtitleAccessory, subtitleAccessory: SubtitleAccessory,
presence: EnginePeer.Presence?, presence: EnginePeer.Presence?,
selectionState: SelectionState, selectionState: SelectionState,
hasNext: Bool, hasNext: Bool,
action: @escaping (EnginePeer) -> Void action: @escaping (EnginePeer) -> Void,
openStories: ((EnginePeer, UIView) -> Void)? = nil
) { ) {
self.context = context self.context = context
self.theme = theme self.theme = theme
@ -78,12 +83,14 @@ public final class PeerListItemComponent: Component {
self.sideInset = sideInset self.sideInset = sideInset
self.title = title self.title = title
self.peer = peer self.peer = peer
self.storyStats = storyStats
self.subtitle = subtitle self.subtitle = subtitle
self.subtitleAccessory = subtitleAccessory self.subtitleAccessory = subtitleAccessory
self.presence = presence self.presence = presence
self.selectionState = selectionState self.selectionState = selectionState
self.hasNext = hasNext self.hasNext = hasNext
self.action = action self.action = action
self.openStories = openStories
} }
public static func ==(lhs: PeerListItemComponent, rhs: PeerListItemComponent) -> Bool { public static func ==(lhs: PeerListItemComponent, rhs: PeerListItemComponent) -> Bool {
@ -108,6 +115,9 @@ public final class PeerListItemComponent: Component {
if lhs.peer != rhs.peer { if lhs.peer != rhs.peer {
return false return false
} }
if lhs.storyStats != rhs.storyStats {
return false
}
if lhs.subtitle != rhs.subtitle { if lhs.subtitle != rhs.subtitle {
return false return false
} }
@ -133,6 +143,7 @@ public final class PeerListItemComponent: Component {
private let label = ComponentView<Empty>() private let label = ComponentView<Empty>()
private let separatorLayer: SimpleLayer private let separatorLayer: SimpleLayer
private let avatarNode: AvatarNode private let avatarNode: AvatarNode
private let avatarButtonView: HighlightTrackingButton
private var avatarIcon: ComponentView<Empty>? private var avatarIcon: ComponentView<Empty>?
private var iconView: UIImageView? private var iconView: UIImageView?
@ -168,7 +179,9 @@ public final class PeerListItemComponent: Component {
self.containerButton = HighlightTrackingButton() self.containerButton = HighlightTrackingButton()
self.avatarNode = AvatarNode(font: avatarFont) self.avatarNode = AvatarNode(font: avatarFont)
self.avatarNode.isLayerBacked = true self.avatarNode.isLayerBacked = false
self.avatarButtonView = HighlightTrackingButton()
super.init(frame: frame) super.init(frame: frame)
@ -177,6 +190,9 @@ public final class PeerListItemComponent: Component {
self.containerButton.layer.addSublayer(self.avatarNode.layer) self.containerButton.layer.addSublayer(self.avatarNode.layer)
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) 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) { required init?(coder: NSCoder) {
@ -190,6 +206,13 @@ public final class PeerListItemComponent: Component {
component.action(peer) 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<Empty>, transition: Transition) -> CGSize { func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
var synchronousLoad = false var synchronousLoad = false
if let hint = transition.userData(TransitionHint.self) { if let hint = transition.userData(TransitionHint.self) {
@ -234,6 +257,8 @@ public final class PeerListItemComponent: Component {
self.component = component self.component = component
self.state = state self.state = state
self.avatarButtonView.isUserInteractionEnabled = component.storyStats != nil
let labelData: (String, Bool) let labelData: (String, Bool)
if let presence = component.presence { if let presence = component.presence {
let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
@ -319,6 +344,8 @@ public final class PeerListItemComponent: Component {
transition.setFrame(layer: self.avatarNode.layer, frame: avatarFrame) transition.setFrame(layer: self.avatarNode.layer, frame: avatarFrame)
} }
transition.setFrame(view: self.avatarButtonView, frame: avatarFrame)
var statusIcon: EmojiStatusComponent.Content? var statusIcon: EmojiStatusComponent.Content?
if let peer = component.peer { if let peer = component.peer {
let clipStyle: AvatarNodeClipStyle let clipStyle: AvatarNodeClipStyle
@ -328,6 +355,17 @@ public final class PeerListItemComponent: Component {
clipStyle = .round 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.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 { if peer.isScam {
statusIcon = .text(color: component.theme.chat.message.incoming.scamColor, string: component.strings.Message_ScamAccount.uppercased()) statusIcon = .text(color: component.theme.chat.message.incoming.scamColor, string: component.strings.Message_ScamAccount.uppercased())

View File

@ -2039,6 +2039,12 @@ public final class StoryItemSetContainerComponent: Component {
return return
} }
self.navigateToPeer(peer: peer, chat: false) self.navigateToPeer(peer: peer, chat: false)
},
openPeerStories: { [weak self] peer, sourceView in
guard let self else {
return
}
self.openPeerStories(peer: peer, sourceView: sourceView)
} }
)), )),
environment: {}, 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() { private func openStoryEditing() {
guard let component = self.component, let peerReference = PeerReference(component.slice.peer._asPeer()) else { guard let component = self.component, let peerReference = PeerReference(component.slice.peer._asPeer()) else {
return return

View File

@ -56,6 +56,7 @@ final class StoryItemSetViewListComponent: Component {
let deleteAction: () -> Void let deleteAction: () -> Void
let moreAction: (UIView, ContextGesture?) -> Void let moreAction: (UIView, ContextGesture?) -> Void
let openPeer: (EnginePeer) -> Void let openPeer: (EnginePeer) -> Void
let openPeerStories: (EnginePeer, UIView) -> Void
init( init(
externalState: ExternalState, externalState: ExternalState,
@ -73,7 +74,8 @@ final class StoryItemSetViewListComponent: Component {
expandViewStats: @escaping () -> Void, expandViewStats: @escaping () -> Void,
deleteAction: @escaping () -> Void, deleteAction: @escaping () -> Void,
moreAction: @escaping (UIView, ContextGesture?) -> Void, moreAction: @escaping (UIView, ContextGesture?) -> Void,
openPeer: @escaping (EnginePeer) -> Void openPeer: @escaping (EnginePeer) -> Void,
openPeerStories: @escaping (EnginePeer, UIView) -> Void
) { ) {
self.externalState = externalState self.externalState = externalState
self.context = context self.context = context
@ -91,6 +93,7 @@ final class StoryItemSetViewListComponent: Component {
self.deleteAction = deleteAction self.deleteAction = deleteAction
self.moreAction = moreAction self.moreAction = moreAction
self.openPeer = openPeer self.openPeer = openPeer
self.openPeerStories = openPeerStories
} }
static func ==(lhs: StoryItemSetViewListComponent, rhs: StoryItemSetViewListComponent) -> Bool { static func ==(lhs: StoryItemSetViewListComponent, rhs: StoryItemSetViewListComponent) -> Bool {
@ -484,6 +487,7 @@ final class StoryItemSetViewListComponent: Component {
sideInset: 0.0, sideInset: 0.0,
title: item.peer.displayTitle(strings: component.strings, displayOrder: .firstLast), title: item.peer.displayTitle(strings: component.strings, displayOrder: .firstLast),
peer: item.peer, peer: item.peer,
storyStats: item.storyStats,
subtitle: dateText, subtitle: dateText,
subtitleAccessory: .checks, subtitleAccessory: .checks,
presence: nil, presence: nil,
@ -494,6 +498,12 @@ final class StoryItemSetViewListComponent: Component {
return return
} }
component.openPeer(peer) component.openPeer(peer)
},
openStories: { [weak self] peer, sourceView in
guard let self, let component = self.component else {
return
}
component.openPeerStories(peer, sourceView)
} }
)), )),
environment: {}, environment: {},

View File

@ -563,6 +563,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
private var powerSavingMonitoringDisposable: Disposable? private var powerSavingMonitoringDisposable: Disposable?
private var avatarNode: ChatAvatarNavigationNode? private var avatarNode: ChatAvatarNavigationNode?
private var storyStats: PeerStoryStats?
public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(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] = []) { public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(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 let _ = ChatControllerCount.modify { value in
@ -1165,7 +1166,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
var expandAvatar = false var expandAvatar = false
if case let .groupParticipant(storyStats, avatarHeaderNode) = source { if case let .groupParticipant(storyStats, avatarHeaderNode) = source {
if let storyStats, storyStats.totalCount != 0, let avatarHeaderNode = avatarHeaderNode as? ChatMessageAvatarHeaderNode { 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 return
} else { } else {
expandAvatar = true expandAvatar = true
@ -4708,8 +4709,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
chatInfoButtonItem = UIBarButtonItem(customDisplayNode: avatarNode)! chatInfoButtonItem = UIBarButtonItem(customDisplayNode: avatarNode)!
self.avatarNode = avatarNode self.avatarNode = avatarNode
//avatarNode.updateStoryView(transition: .immediate, theme: self.presentationData.theme)
case .feed: case .feed:
chatInfoButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) 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)?.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.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.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,9 +12247,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
@objc func rightNavigationButtonAction() { @objc func rightNavigationButtonAction() {
if let button = self.rightNavigationButton { if let button = self.rightNavigationButton {
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) self.navigationButtonAction(button.action)
} }
} }
}
@objc private func moreButtonPressed() { @objc private func moreButtonPressed() {
self.moreBarButton.play() self.moreBarButton.play()
@ -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 = StoryContentContextImpl(context: self.context, isHidden: false, focusedPeerId: peerId, singlePeer: true)
let _ = (storyContent.state let _ = (storyContent.state
|> filter { $0.slice != nil } |> filter { $0.slice != nil }
|> take(1) |> take(1)
|> deliverOnMainQueue).start(next: { [weak self, weak avatarHeaderNode] _ in |> deliverOnMainQueue).start(next: { [weak self, weak avatarHeaderNode, weak avatarNode] _ in
guard let self else { guard let self else {
return return
} }
@ -17030,6 +17051,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
sourceIsAvatar: false sourceIsAvatar: false
) )
avatarHeaderNode.avatarNode.isHidden = true 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( let storyContainerScreen = StoryContainerScreen(
@ -17037,9 +17066,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
content: storyContent, content: storyContent,
transitionIn: transitionIn, transitionIn: transitionIn,
transitionOut: { peerId, _ in transitionOut: { peerId, _ in
guard let avatarHeaderNode else { if let avatarHeaderNode {
return nil
}
let destinationView = avatarHeaderNode.avatarNode.view let destinationView = avatarHeaderNode.avatarNode.view
return StoryContainerScreen.TransitionOut( return StoryContainerScreen.TransitionOut(
destinationView: destinationView, destinationView: destinationView,
@ -17071,6 +17098,41 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
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) self.push(storyContainerScreen)

View File

@ -613,7 +613,11 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode {
unseenCount: storyStats.unseenCount, unseenCount: storyStats.unseenCount,
hasUnseenCloseFriendsItems: false hasUnseenCloseFriendsItems: false
) )
}, theme: theme, transition: .immediate) }, presentationParams: AvatarNode.StoryPresentationParams(
colors: AvatarNode.Colors(theme: theme),
lineWidth: 2.0,
inactiveLineWidth: 2.0
), transition: .immediate)
} }
} }

View File

@ -278,15 +278,16 @@ class ChatMessageStoryMentionContentNode: ChatMessageBubbleContentNode {
} }
let indicatorFrame = imageFrame 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( let _ = strongSelf.storyIndicator.update(
transition: .immediate, transition: .immediate,
component: AnyComponent(AvatarStoryIndicatorComponent( component: AnyComponent(AvatarStoryIndicatorComponent(
hasUnseen: hasUnseen, hasUnseen: hasUnseen,
hasUnseenCloseFriendsItems: hasUnseen && (story?.isCloseFriends ?? false), hasUnseenCloseFriendsItems: hasUnseen && (story?.isCloseFriends ?? false),
theme: item.presentationData.theme.theme, colors: storyColors,
activeLineWidth: 3.0, activeLineWidth: 3.0,
inactiveLineWidth: 1.0 + UIScreenPixel, inactiveLineWidth: 1.0 + UIScreenPixel,
isGlassBackground: true,
counters: nil counters: nil
)), )),
environment: {}, environment: {},

View File

@ -26,6 +26,7 @@ final class PeerInfoScreenMemberItem: PeerInfoScreenItem {
let isAccount: Bool let isAccount: Bool
let action: ((PeerInfoScreenMemberItemAction) -> Void)? let action: ((PeerInfoScreenMemberItemAction) -> Void)?
let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
let openStories: ((UIView) -> Void)?
init( init(
id: AnyHashable, id: AnyHashable,
@ -35,7 +36,8 @@ final class PeerInfoScreenMemberItem: PeerInfoScreenItem {
badge: String? = nil, badge: String? = nil,
isAccount: Bool, isAccount: Bool,
action: ((PeerInfoScreenMemberItemAction) -> Void)?, action: ((PeerInfoScreenMemberItemAction) -> Void)?,
contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil,
openStories: ((UIView) -> Void)? = nil
) { ) {
self.id = id self.id = id
self.context = context self.context = context
@ -45,6 +47,7 @@ final class PeerInfoScreenMemberItem: PeerInfoScreenItem {
self.isAccount = isAccount self.isAccount = isAccount
self.action = action self.action = action
self.contextAction = contextAction self.contextAction = contextAction
self.openStories = openStories
} }
func node() -> PeerInfoScreenItemNode { func node() -> PeerInfoScreenItemNode {
@ -195,7 +198,12 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode {
}, removePeer: { _ in }, 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) let params = ListViewItemLayoutParams(width: width, leftInset: safeInsets.left, rightInset: safeInsets.right, availableHeight: 1000.0)

View File

@ -26,6 +26,7 @@ enum PeerMembersListAction {
case promote case promote
case restrict case restrict
case remove case remove
case openStories(sourceView: UIView)
} }
private enum PeerMembersListEntryStableId: Hashable { private enum PeerMembersListEntryStableId: Hashable {
@ -35,7 +36,7 @@ private enum PeerMembersListEntryStableId: Hashable {
private enum PeerMembersListEntry: Comparable, Identifiable { private enum PeerMembersListEntry: Comparable, Identifiable {
case addMember(PresentationTheme, String) case addMember(PresentationTheme, String)
case member(PresentationTheme, Int, PeerInfoMember) case member(theme: PresentationTheme, index: Int, member: PeerInfoMember)
var stableId: PeerMembersListEntryStableId { var stableId: PeerMembersListEntryStableId {
switch self { switch self {
@ -126,7 +127,9 @@ private enum PeerMembersListEntry: Comparable, Identifiable {
action(member, .open) action(member, .open)
}, setPeerIdWithRevealedOptions: { _, _ in }, setPeerIdWithRevealedOptions: { _, _ in
}, removePeer: { _ 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)) entries.append(.addMember(presentationData.theme, presentationData.strings.GroupInfo_AddParticipant))
} }
for member in state.members { 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 let transaction = preparedTransition(from: self.currentEntries, to: entries, context: self.context, presentationData: presentationData, enclosingPeer: enclosingPeer, addMemberAction: { [weak self] in

View File

@ -1106,7 +1106,7 @@ func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: Peer?, member:
result.insert(.promote) result.insert(.promote)
} else { } else {
switch member { switch member {
case let .channelMember(channelMember): case let .channelMember(channelMember, _):
switch channelMember.participant { switch channelMember.participant {
case .creator: case .creator:
break break
@ -1142,7 +1142,7 @@ func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: Peer?, member:
result.insert(.promote) result.insert(.promote)
case .admin: case .admin:
switch member { switch member {
case let .legacyGroupMember(_, _, invitedBy, _): case let .legacyGroupMember(_, _, invitedBy, _, _):
result.insert(.restrict) result.insert(.restrict)
if invitedBy == accountPeerId { if invitedBy == accountPeerId {
result.insert(.promote) result.insert(.promote)
@ -1154,7 +1154,7 @@ func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: Peer?, member:
} }
case .member: case .member:
switch member { switch member {
case let .legacyGroupMember(_, _, invitedBy, _): case let .legacyGroupMember(_, _, invitedBy, _, _):
if invitedBy == accountPeerId { if invitedBy == accountPeerId {
result.insert(.restrict) result.insert(.restrict)
} }

View File

@ -379,13 +379,15 @@ final class PeerInfoHeaderNavigationTransition {
let sourceTitleView: ChatTitleView let sourceTitleView: ChatTitleView
let sourceTitleFrame: CGRect let sourceTitleFrame: CGRect
let sourceSubtitleFrame: CGRect let sourceSubtitleFrame: CGRect
let previousAvatarView: UIView?
let fraction: CGFloat 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.sourceNavigationBar = sourceNavigationBar
self.sourceTitleView = sourceTitleView self.sourceTitleView = sourceTitleView
self.sourceTitleFrame = sourceTitleFrame self.sourceTitleFrame = sourceTitleFrame
self.sourceSubtitleFrame = sourceSubtitleFrame self.sourceSubtitleFrame = sourceSubtitleFrame
self.previousAvatarView = previousAvatarView
self.fraction = fraction self.fraction = fraction
} }
} }
@ -455,40 +457,22 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
} }
func updateStoryView(transition: ContainedViewLayoutTransition, theme: PresentationTheme) { func updateStoryView(transition: ContainedViewLayoutTransition, theme: PresentationTheme) {
if let storyData = self.storyData { var colors = AvatarNode.Colors(theme: theme)
let avatarStoryView: ComponentView<Empty> colors.seenColors = [
if let current = self.avatarStoryView { theme.list.controlSecondaryColor,
avatarStoryView = current theme.list.controlSecondaryColor
} else { ]
avatarStoryView = ComponentView() self.avatarNode.setStoryStats(storyStats: self.storyData.flatMap { storyData in
self.avatarStoryView = avatarStoryView return AvatarNode.StoryStats(
} totalCount: 1,
unseenCount: storyData.hasUnseen ? 1 : 0,
let _ = avatarStoryView.update( hasUnseenCloseFriendsItems: storyData.hasUnseenCloseFriends
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
) )
if let avatarStoryComponentView = avatarStoryView.view { }, presentationParams: AvatarNode.StoryPresentationParams(
if avatarStoryComponentView.superview == nil { colors: colors,
self.containerNode.view.insertSubview(avatarStoryComponentView, at: 0) lineWidth: 3.0,
} inactiveLineWidth: 1.5
avatarStoryComponentView.frame = self.avatarNode.frame ), transition: Transition(transition))
}
} else {
if let avatarStoryView = self.avatarStoryView {
self.avatarStoryView = nil
avatarStoryView.view?.removeFromSuperview()
}
}
} }
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
@ -610,11 +594,11 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
avatarCornerRadius = avatarSize / 2.0 avatarCornerRadius = avatarSize / 2.0
} }
if self.avatarNode.layer.cornerRadius != 0.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 { } 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 self.isFirstAvatarLoading = false
@ -1175,7 +1159,6 @@ final class PeerInfoAvatarListNode: ASDisplayNode {
self.containerNode = ASDisplayNode() self.containerNode = ASDisplayNode()
self.bottomCoverNode = ASDisplayNode() self.bottomCoverNode = ASDisplayNode()
self.bottomCoverNode.backgroundColor = .black
self.maskNode = DynamicIslandMaskNode() self.maskNode = DynamicIslandMaskNode()
self.pinchSourceNode = PinchSourceContainerNode() self.pinchSourceNode = PinchSourceContainerNode()
@ -2918,6 +2901,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
} else { } else {
transitionSourceAvatarFrame = avatarNavigationNode.avatarNode.view.convert(avatarNavigationNode.avatarNode.view.bounds, to: navigationTransition.sourceNavigationBar.view) 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 { } else {
if deviceMetrics.hasDynamicIsland && !isLandscape { 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) 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 let avatarOffset: CGFloat
if self.navigationTransition != nil { if self.navigationTransition != nil {
if let transitionSourceAvatarFrame = transitionSourceAvatarFrame { 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 { } else {
avatarScale = 1.0 avatarScale = 1.0
} }
@ -3380,6 +3370,14 @@ final class PeerInfoHeaderNode: ASDisplayNode {
avatarOffset = apparentTitleLockOffset + 0.0 * (1.0 - titleCollapseFraction) + 10.0 * titleCollapseFraction 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 { if subtitleIsButton {
subtitleFrame.origin.y += 11.0 * (1.0 - titleCollapseFraction) subtitleFrame.origin.y += 11.0 * (1.0 - titleCollapseFraction)
if let subtitleBackgroundButton = self.subtitleBackgroundButton { if let subtitleBackgroundButton = self.subtitleBackgroundButton {
@ -3398,8 +3396,14 @@ final class PeerInfoHeaderNode: ASDisplayNode {
if self.isAvatarExpanded { if self.isAvatarExpanded {
self.avatarListNode.listContainerNode.isHidden = false self.avatarListNode.listContainerNode.isHidden = false
if let transitionSourceAvatarFrame = transitionSourceAvatarFrame { if let transitionSourceAvatarFrame = transitionSourceAvatarFrame {
transition.updateCornerRadius(node: self.avatarListNode.listContainerNode, cornerRadius: transitionFraction * transitionSourceAvatarFrame.width / 2.0) var trueAvatarSize = transitionSourceAvatarFrame.size
transition.updateCornerRadius(node: self.avatarListNode.listContainerNode.controlsClippingNode, cornerRadius: transitionFraction * transitionSourceAvatarFrame.width / 2.0) 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 { } else {
transition.updateCornerRadius(node: self.avatarListNode.listContainerNode, cornerRadius: 0.0) transition.updateCornerRadius(node: self.avatarListNode.listContainerNode, cornerRadius: 0.0)
transition.updateCornerRadius(node: self.avatarListNode.listContainerNode.controlsClippingNode, 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) transition.updateAlpha(layer: avatarStoryView.layer, alpha: 1.0 - transitionFraction)
} }
let apparentAvatarFrame: CGRect var apparentAvatarFrame: CGRect
let controlsClippingFrame: CGRect let controlsClippingFrame: CGRect
if self.isAvatarExpanded { if self.isAvatarExpanded {
let expandedAvatarCenter = CGPoint(x: expandedAvatarListSize.width / 2.0, y: expandedAvatarListSize.height / 2.0 - contentOffset / 2.0) 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()) 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 { 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) 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 { } else {
controlsClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: expandedAvatarListSize) controlsClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: expandedAvatarListSize)
} }
} else { } 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 controlsClippingFrame = apparentAvatarFrame
} }
@ -3466,7 +3482,13 @@ final class PeerInfoHeaderNode: ASDisplayNode {
if self.isAvatarExpanded { if self.isAvatarExpanded {
if let transitionSourceAvatarFrame = transitionSourceAvatarFrame { if let transitionSourceAvatarFrame = transitionSourceAvatarFrame {
let neutralAvatarListContainerSize = expandedAvatarListSize 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) avatarListContainerFrame = CGRect(origin: CGPoint(x: -avatarListContainerSize.width / 2.0, y: -avatarListContainerSize.height / 2.0), size: avatarListContainerSize)
} else { } else {
avatarListContainerFrame = CGRect(origin: CGPoint(x: -expandedAvatarListSize.width / 2.0, y: -expandedAvatarListSize.height / 2.0), size: expandedAvatarListSize) 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.topCoverNode.update(maskValue)
self.avatarListNode.maskNode.update(maskValue) self.avatarListNode.maskNode.update(maskValue)
self.avatarListNode.bottomCoverNode.backgroundColor = UIColor(white: 0.0, alpha: maskValue)
self.avatarListNode.listContainerNode.topShadowNode.isHidden = !self.isAvatarExpanded self.avatarListNode.listContainerNode.topShadowNode.isHidden = !self.isAvatarExpanded
@ -3520,7 +3543,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
avatarMaskOffset -= contentOffset 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.maskNode.bounds = CGRect(origin: .zero, size: CGSize(width: 171.0, height: 171.0))
self.avatarListNode.bottomCoverNode.position = self.avatarListNode.maskNode.position self.avatarListNode.bottomCoverNode.position = self.avatarListNode.maskNode.position

View File

@ -12,15 +12,15 @@ enum PeerInfoMemberRole {
} }
enum PeerInfoMember: Equatable { enum PeerInfoMember: Equatable {
case channelMember(RenderedChannelParticipant) case channelMember(participant: RenderedChannelParticipant, storyStats: PeerStoryStats?)
case legacyGroupMember(peer: RenderedPeer, role: PeerInfoMemberRole, invitedBy: PeerId?, presence: TelegramUserPresence?) case legacyGroupMember(peer: RenderedPeer, role: PeerInfoMemberRole, invitedBy: PeerId?, presence: TelegramUserPresence?, storyStats: PeerStoryStats?)
case account(peer: RenderedPeer) case account(peer: RenderedPeer)
var id: PeerId { var id: PeerId {
switch self { switch self {
case let .channelMember(channelMember): case let .channelMember(participant, _):
return channelMember.peer.id return participant.peer.id
case let .legacyGroupMember(peer, _, _, _): case let .legacyGroupMember(peer, _, _, _, _):
return peer.peerId return peer.peerId
case let .account(peer): case let .account(peer):
return peer.peerId return peer.peerId
@ -29,9 +29,9 @@ enum PeerInfoMember: Equatable {
var peer: Peer { var peer: Peer {
switch self { switch self {
case let .channelMember(channelMember): case let .channelMember(participant, _):
return channelMember.peer return participant.peer
case let .legacyGroupMember(peer, _, _, _): case let .legacyGroupMember(peer, _, _, _, _):
return peer.peers[peer.peerId]! return peer.peers[peer.peerId]!
case let .account(peer): case let .account(peer):
return peer.peers[peer.peerId]! return peer.peers[peer.peerId]!
@ -40,9 +40,9 @@ enum PeerInfoMember: Equatable {
var presence: TelegramUserPresence? { var presence: TelegramUserPresence? {
switch self { switch self {
case let .channelMember(channelMember): case let .channelMember(participant, _):
return channelMember.presences[channelMember.peer.id] as? TelegramUserPresence return participant.presences[participant.peer.id] as? TelegramUserPresence
case let .legacyGroupMember(_, _, _, presence): case let .legacyGroupMember(_, _, _, presence, _):
return presence return presence
case .account: case .account:
return nil return nil
@ -51,8 +51,8 @@ enum PeerInfoMember: Equatable {
var role: PeerInfoMemberRole { var role: PeerInfoMemberRole {
switch self { switch self {
case let .channelMember(channelMember): case let .channelMember(participant, _):
switch channelMember.participant { switch participant.participant {
case .creator: case .creator:
return .creator return .creator
case let .member(_, _, adminInfo, _, _): case let .member(_, _, adminInfo, _, _):
@ -62,7 +62,7 @@ enum PeerInfoMember: Equatable {
return .member return .member
} }
} }
case let .legacyGroupMember(_, role, _, _): case let .legacyGroupMember(_, role, _, _, _):
return role return role
case .account: case .account:
return .member return .member
@ -71,8 +71,8 @@ enum PeerInfoMember: Equatable {
var rank: String? { var rank: String? {
switch self { switch self {
case let .channelMember(channelMember): case let .channelMember(participant, _):
switch channelMember.participant { switch participant.participant {
case let .creator(_, _, rank): case let .creator(_, _, rank):
return rank return rank
case let .member(_, _, _, _, rank): case let .member(_, _, _, _, rank):
@ -84,6 +84,17 @@ enum PeerInfoMember: Equatable {
return nil 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 { enum PeerInfoMembersDataState: Equatable {
@ -154,7 +165,9 @@ private final class PeerInfoMembersContextImpl {
guard let strongSelf = self else { guard let strongSelf = self else {
return 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] let members: [PeerInfoMember]
if unsortedMembers.count <= 50 { if unsortedMembers.count <= 50 {
members = membersSortedByPresence(unsortedMembers, accountPeerId: strongSelf.context.account.peerId) members = membersSortedByPresence(unsortedMembers, accountPeerId: strongSelf.context.account.peerId)
@ -230,7 +243,7 @@ private final class PeerInfoMembersContextImpl {
role = .member role = .member
invitedBy = invitedByValue 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]))
} }
} }

View File

@ -448,6 +448,7 @@ private enum PeerInfoMemberAction {
case promote case promote
case restrict case restrict
case remove case remove
case openStories(sourceView: UIView)
} }
private enum PeerInfoContextSubject { private enum PeerInfoContextSubject {
@ -1389,6 +1390,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
case .remove: case .remove:
interaction.performMemberAction(member, .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) strongSelf.performMemberAction(member: member, action: .restrict)
case .remove: case .remove:
strongSelf.performMemberAction(member: member, action: .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 { switch action {
case .promote: case .promote:
if case let .channelMember(channelMember) = member { if case let .channelMember(channelMember, _) = member {
var upgradedToSupergroupImpl: (() -> Void)? var upgradedToSupergroupImpl: (() -> Void)?
let controller = channelAdminController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id, adminId: member.id, initialParticipant: channelMember.participant, updated: { _ in let controller = channelAdminController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id, adminId: member.id, initialParticipant: channelMember.participant, updated: { _ in
}, upgradedToSupergroup: { _, f in }, upgradedToSupergroup: { _, f in
@ -7116,7 +7121,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
} }
} }
case .restrict: case .restrict:
if case let .channelMember(channelMember) = member { if case let .channelMember(channelMember, _) = member {
var upgradedToSupergroupImpl: (() -> Void)? var upgradedToSupergroupImpl: (() -> Void)?
let controller = channelBannedMemberController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id, memberId: member.id, initialParticipant: channelMember.participant, updated: { _ in 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: case .remove:
data.members?.membersContext.removeMember(memberId: member.id) 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 previousTitleNode: (ASDisplayNode, PortalView)?
private var previousStatusNode: (ASDisplayNode, ASDisplayNode)? private var previousStatusNode: (ASDisplayNode, ASDisplayNode)?
private var previousAvatarView: UIView?
private var didSetup: Bool = false private var didSetup: Bool = false
init(screenNode: PeerInfoScreenNode, presentationData: PresentationData, headerNode: PeerInfoHeaderNode) { init(screenNode: PeerInfoScreenNode, presentationData: PresentationData, headerNode: PeerInfoHeaderNode) {
@ -10576,7 +10648,9 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig
self.view.layer.addSublayer(previousRightButton) self.view.layer.addSublayer(previousRightButton)
} }
} else { } 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() { } else if let previousRightButton = bottomNavigationBar.rightButtonNode.view.layer.snapshotContentTree() {
self.previousRightButton = previousRightButton self.previousRightButton = previousRightButton
self.view.layer.addSublayer(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 previousTitleFrame = previousTitleView.titleContainerView.convert(previousTitleView.titleContainerView.bounds, to: bottomNavigationBar.view)
let previousStatusFrame = previousTitleView.activityNode.view.convert(previousTitleView.activityNode.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 var topHeight = topNavigationBar.backgroundNode.bounds.height
if let iconView = previousTitleView.titleCredibilityIconView.componentView { if let iconView = previousTitleView.titleCredibilityIconView.componentView {

View File

@ -48,16 +48,9 @@ public extension ChannelParticipant {
} }
public struct ChannelMemberListState { public struct ChannelMemberListState {
public let list: [RenderedChannelParticipant] public var list: [RenderedChannelParticipant]
public let loadingState: ChannelMemberListLoadingState public var peerStoryStats: [EnginePeer.Id: PeerStoryStats]
public var 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)
}
} }
enum ChannelMemberListCategory { enum ChannelMemberListCategory {
@ -72,7 +65,7 @@ enum ChannelMemberListCategory {
} }
private protocol ChannelMemberCategoryListContext { private protocol ChannelMemberCategoryListContext {
var listStateValue: ChannelMemberListState { get } //var listStateValue: ChannelMemberListState { get }
var listState: Signal<ChannelMemberListState, NoError> { get } var listState: Signal<ChannelMemberListState, NoError> { get }
func loadMore() func loadMore()
func reset(_ force: Bool) func reset(_ force: Bool)
@ -139,7 +132,20 @@ private final class ChannelMemberSingleCategoryListContext: ChannelMemberCategor
} }
private var listStatePromise: Promise<ChannelMemberListState> private var listStatePromise: Promise<ChannelMemberListState>
var listState: Signal<ChannelMemberListState, NoError> { var listState: Signal<ChannelMemberListState, NoError> {
let postbox = self.postbox
return self.listStatePromise.get() return self.listStatePromise.get()
|> mapToSignal { state -> Signal<ChannelMemberListState, NoError> 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() private let loadingDisposable = MetaDisposable()
@ -155,7 +161,7 @@ private final class ChannelMemberSingleCategoryListContext: ChannelMemberCategor
self.peerId = peerId self.peerId = peerId
self.category = category 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.listStatePromise = Promise(self.listStateValue)
self.loadMoreInternal(initial: true) self.loadMoreInternal(initial: true)
} }
@ -182,7 +188,7 @@ private final class ChannelMemberSingleCategoryListContext: ChannelMemberCategor
loadCount = requestBatchSize loadCount = requestBatchSize
} }
self.listStateValue = self.listStateValue.withUpdatedLoadingState(.loading(initial: initial)) self.listStateValue.loadingState = .loading(initial: initial)
self.loadingDisposable.set((self.loadMoreSignal(count: loadCount) self.loadingDisposable.set((self.loadMoreSignal(count: loadCount)
|> deliverOnMainQueue).start(next: { [weak self] members in |> deliverOnMainQueue).start(next: { [weak self] members in
@ -201,7 +207,10 @@ private final class ChannelMemberSingleCategoryListContext: ChannelMemberCategor
} }
self.loadingDisposable.set(nil) 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.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 { if case .loading = self.listStateValue.loadingState {
self.loadMore() self.loadMore()
} }
@ -290,7 +303,12 @@ private final class ChannelMemberSingleCategoryListContext: ChannelMemberCategor
list.append(member) 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 { if firstLoad {
self.checkUpdateHead() self.checkUpdateHead()
} }
@ -534,7 +552,9 @@ private final class ChannelMemberSingleCategoryListContext: ChannelMemberCategor
} }
} }
if updatedList { 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 final class ChannelMemberMultiCategoryListContext: ChannelMemberCategoryListContext {
private var contexts: [ChannelMemberSingleCategoryListContext] = [] private var contexts: [ChannelMemberSingleCategoryListContext] = []
var listStateValue: ChannelMemberListState { /*var listStateValue: ChannelMemberListState {
return ChannelMemberMultiCategoryListContext.reduceListStates(self.contexts.map { $0.listStateValue }) return ChannelMemberMultiCategoryListContext.reduceListStates(self.contexts.map { $0.listStateValue })
} }*/
private static func reduceListStates(_ listStates: [ChannelMemberListState]) -> ChannelMemberListState { private static func reduceListStates(_ listStates: [ChannelMemberListState]) -> ChannelMemberListState {
var allReady = true var allReady = true
@ -555,12 +575,13 @@ private final class ChannelMemberMultiCategoryListContext: ChannelMemberCategory
} }
} }
if !allReady { if !allReady {
return ChannelMemberListState(list: [], loadingState: .loading(initial: true)) return ChannelMemberListState(list: [], peerStoryStats: [:], loadingState: .loading(initial: true))
} }
var list: [RenderedChannelParticipant] = [] var list: [RenderedChannelParticipant] = []
var existingIds = Set<PeerId>() var existingIds = Set<PeerId>()
var loadingState: ChannelMemberListLoadingState = .ready(hasMore: false) var loadingState: ChannelMemberListLoadingState = .ready(hasMore: false)
var peerStoryStats: [PeerId: PeerStoryStats] = [:]
loop: for i in 0 ..< listStates.count { loop: for i in 0 ..< listStates.count {
for item in listStates[i].list { for item in listStates[i].list {
if !existingIds.contains(item.peer.id) { if !existingIds.contains(item.peer.id) {
@ -568,6 +589,9 @@ private final class ChannelMemberMultiCategoryListContext: ChannelMemberCategory
list.append(item) list.append(item)
} }
} }
for (id, value) in listStates[i].peerStoryStats {
peerStoryStats[id] = value
}
switch listStates[i].loadingState { switch listStates[i].loadingState {
case let .loading(initial): case let .loading(initial):
loadingState = .loading(initial: initial) loadingState = .loading(initial: initial)
@ -579,7 +603,7 @@ private final class ChannelMemberMultiCategoryListContext: ChannelMemberCategory
} }
} }
} }
return ChannelMemberListState(list: list, loadingState: loadingState) return ChannelMemberListState(list: list, peerStoryStats: peerStoryStats, loadingState: loadingState)
} }
var listState: Signal<ChannelMemberListState, NoError> { var listState: Signal<ChannelMemberListState, NoError> {
@ -639,6 +663,7 @@ private final class PeerChannelMemberContextWithSubscribers {
private let subscribers = Bag<(ChannelMemberListState) -> Void>() private let subscribers = Bag<(ChannelMemberListState) -> Void>()
private let disposable = MetaDisposable() private let disposable = MetaDisposable()
private let becameEmpty: () -> Void private let becameEmpty: () -> Void
private var currentValue: ChannelMemberListState?
private var emptyTimer: SwiftSignalKit.Timer? private var emptyTimer: SwiftSignalKit.Timer?
@ -649,6 +674,7 @@ private final class PeerChannelMemberContextWithSubscribers {
self.disposable.set((context.listState self.disposable.set((context.listState
|> deliverOnMainQueue).start(next: { [weak self] value in |> deliverOnMainQueue).start(next: { [weak self] value in
if let strongSelf = self { if let strongSelf = self {
strongSelf.currentValue = value
for f in strongSelf.subscribers.copyItems() { for f in strongSelf.subscribers.copyItems() {
f(value) f(value)
} }
@ -678,7 +704,9 @@ private final class PeerChannelMemberContextWithSubscribers {
func subscribe(requestUpdate: Bool, updated: @escaping (ChannelMemberListState) -> Void) -> Disposable { func subscribe(requestUpdate: Bool, updated: @escaping (ChannelMemberListState) -> Void) -> Disposable {
let wasEmpty = self.subscribers.isEmpty let wasEmpty = self.subscribers.isEmpty
let index = self.subscribers.add(updated) let index = self.subscribers.add(updated)
updated(self.context.listStateValue) if let currentValue = self.currentValue {
updated(currentValue)
}
if wasEmpty { if wasEmpty {
self.emptyTimer?.invalidate() self.emptyTimer?.invalidate()
if requestUpdate { if requestUpdate {

View File

@ -707,7 +707,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
component: AnyComponent(AvatarStoryIndicatorComponent( component: AnyComponent(AvatarStoryIndicatorComponent(
hasUnseen: true, hasUnseen: true,
hasUnseenCloseFriendsItems: false, hasUnseenCloseFriendsItems: false,
theme: defaultDarkPresentationTheme, colors: AvatarStoryIndicatorComponent.Colors(theme: defaultDarkPresentationTheme),
activeLineWidth: 1.0 + UIScreenPixel, activeLineWidth: 1.0 + UIScreenPixel,
inactiveLineWidth: 1.0 + UIScreenPixel, inactiveLineWidth: 1.0 + UIScreenPixel,
counters: nil counters: nil