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

View File

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

View File

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

View File

@ -355,8 +355,10 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem {
let shimmering: ItemListPeerItemShimmering?
let displayDecorations: Bool
let disableInteractiveTransitionIfNecessary: Bool
let storyStats: PeerStoryStats?
let openStories: ((UIView) -> Void)?
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: EnginePeer, threadInfo: EngineMessageHistoryThread.Info? = nil, height: ItemListPeerItemHeight = .peerList, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, nameStyle: ItemListPeerItemNameStyle = .distinctBold, presence: EnginePeer.Presence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, highlighted: Bool = false, selectable: Bool, highlightable: Bool = true, animateFirstAvatarTransition: Bool = true, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, removePeer: @escaping (EnginePeer.Id) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, hasTopStripe: Bool = true, hasTopGroupInset: Bool = true, noInsets: Bool = false, noCorners: Bool = false, tag: ItemListItemTag? = nil, header: ListViewItemHeader? = nil, shimmering: ItemListPeerItemShimmering? = nil, displayDecorations: Bool = true, disableInteractiveTransitionIfNecessary: Bool = false) {
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: EnginePeer, threadInfo: EngineMessageHistoryThread.Info? = nil, height: ItemListPeerItemHeight = .peerList, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, nameStyle: ItemListPeerItemNameStyle = .distinctBold, presence: EnginePeer.Presence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, highlighted: Bool = false, selectable: Bool, highlightable: Bool = true, animateFirstAvatarTransition: Bool = true, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, removePeer: @escaping (EnginePeer.Id) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, hasTopStripe: Bool = true, hasTopGroupInset: Bool = true, noInsets: Bool = false, noCorners: Bool = false, tag: ItemListItemTag? = nil, header: ListViewItemHeader? = nil, shimmering: ItemListPeerItemShimmering? = nil, displayDecorations: Bool = true, disableInteractiveTransitionIfNecessary: Bool = false, storyStats: PeerStoryStats? = nil, openStories: ((UIView) -> Void)? = nil) {
self.presentationData = presentationData
self.dateTimeFormat = dateTimeFormat
self.nameDisplayOrder = nameDisplayOrder
@ -393,6 +395,8 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem {
self.shimmering = shimmering
self.displayDecorations = displayDecorations
self.disableInteractiveTransitionIfNecessary = disableInteractiveTransitionIfNecessary
self.storyStats = storyStats
self.openStories = openStories
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
@ -471,6 +475,8 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
private var avatarIconComponent: EmojiStatusComponent?
private var avatarIconView: ComponentView<Empty>?
private var avatarButton: HighlightTrackingButton?
private let titleNode: TextNode
private let labelNode: TextNode
private let labelBadgeNode: ASImageNode
@ -1250,6 +1256,22 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
let avatarFrame = CGRect(origin: CGPoint(x: params.leftInset + additionalLeftInset + revealOffset + editingOffset + 15.0, y: floorToScreenPixels((layout.contentSize.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
transition.updateFrame(node: strongSelf.avatarNode, frame: avatarFrame)
if item.storyStats != nil {
let avatarButton: HighlightTrackingButton
if let current = strongSelf.avatarButton {
avatarButton = current
} else {
avatarButton = HighlightTrackingButton()
strongSelf.avatarButton = avatarButton
strongSelf.containerNode.view.addSubview(avatarButton)
avatarButton.addTarget(strongSelf, action: #selector(strongSelf.avatarButtonPressed), for: .touchUpInside)
}
avatarButton.frame = avatarFrame
} else if let avatarButton = strongSelf.avatarButton {
strongSelf.avatarButton = nil
avatarButton.removeFromSuperview()
}
if let switchValue = item.switchValue, case .leftCheck = switchValue.style {
let leftCheckNode: CheckNode
if let current = strongSelf.leftCheckNode {
@ -1332,6 +1354,17 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
clipStyle = .roundedRect
}
strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: item.peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, clipStyle: clipStyle, synchronousLoad: synchronousLoad)
strongSelf.avatarNode.setStoryStats(storyStats: item.storyStats.flatMap { storyStats in
return AvatarNode.StoryStats(
totalCount: storyStats.totalCount,
unseenCount: storyStats.unseenCount,
hasUnseenCloseFriendsItems: false
)
}, presentationParams: AvatarNode.StoryPresentationParams(
colors: AvatarNode.Colors(theme: item.presentationData.theme),
lineWidth: 1.33,
inactiveLineWidth: 1.33
), transition: .immediate)
}
}
@ -1443,10 +1476,14 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
self.isHighlighted = highlighted
if let avatarButton = self.avatarButton, avatarButton.bounds.contains(self.view.convert(point, to: avatarButton)) {
self.isHighlighted = false
} else {
self.isHighlighted = highlighted
self.updateIsHighlighted(transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate)
self.updateIsHighlighted(transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate)
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
@ -1513,7 +1550,11 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
transition.updateFrame(node: self.labelBadgeNode, frame: CGRect(origin: CGPoint(x: offset + params.width - rightLabelInset - badgeWidth, y: self.labelBadgeNode.frame.minY), size: CGSize(width: badgeWidth, height: badgeDiameter)))
transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + editingOffset + params.leftInset + 15.0, y: self.avatarNode.frame.minY), size: self.avatarNode.bounds.size))
let avatarFrame = CGRect(origin: CGPoint(x: revealOffset + editingOffset + params.leftInset + 15.0, y: self.avatarNode.frame.minY), size: self.avatarNode.bounds.size)
transition.updateFrame(node: self.avatarNode, frame: avatarFrame)
if let avatarButton = self.avatarButton {
avatarButton.frame = avatarFrame
}
if let avatarIconComponentView = self.avatarIconView?.view {
let avatarFrame = self.avatarNode.frame
@ -1580,6 +1621,13 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
}
return false
}
@objc private func avatarButtonPressed() {
guard let item = self.layoutParams?.0 else {
return
}
item.openStories?(self.avatarNode.view)
}
}
public final class ItemListPeerItemHeader: ListViewItemHeader {

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 messages = PeerViewComponents(rawValue: 1 << 2)
public static let groupId = PeerViewComponents(rawValue: 1 << 3)
public static let storyStats = PeerViewComponents(rawValue: 1 << 4)
public static let all: PeerViewComponents = [.cachedData, .subPeers, .messages, .groupId]
public static let all: PeerViewComponents = [.cachedData, .subPeers, .messages, .groupId, .storyStats]
}
final class MutablePeerView: MutablePostboxView {
@ -27,6 +28,8 @@ final class MutablePeerView: MutablePostboxView {
var media: [MediaId: Media] = [:]
var peerIsContact: Bool
var groupId: PeerGroupId?
var storyStats: PeerStoryStats?
var memberStoryStats: [PeerId: PeerStoryStats] = [:]
init(postbox: PostboxImpl, peerId: PeerId, components: PeerViewComponents) {
self.components = components
@ -54,8 +57,10 @@ final class MutablePeerView: MutablePostboxView {
}
self.cachedData = postbox.cachedPeerDataTable.get(contactPeerId)
self.peerIsContact = postbox.contactsTable.isContact(peerId: self.contactPeerId)
var cachedDataPeerIds = Set<PeerId>()
if let cachedData = self.cachedData {
peerIds.formUnion(cachedData.peerIds)
cachedDataPeerIds = cachedData.peerIds
peerIds.formUnion(cachedDataPeerIds)
messageIds.formUnion(cachedData.messageIds)
}
for id in peerIds {
@ -66,6 +71,11 @@ final class MutablePeerView: MutablePostboxView {
self.peerPresences[id] = presence
}
}
for id in cachedDataPeerIds {
if let value = fetchPeerStoryStats(postbox: postbox, peerId: id) {
self.memberStoryStats[id] = value
}
}
if let peer = self.peers[peerId], let associatedPeerId = peer.associatedPeerId {
if let peer = getPeer(associatedPeerId) {
self.peers[associatedPeerId] = peer
@ -83,6 +93,10 @@ final class MutablePeerView: MutablePostboxView {
}
}
self.media = renderAssociatedMediaForPeers(postbox: postbox, peers: self.peers)
if components.contains(.storyStats) {
self.storyStats = fetchPeerStoryStats(postbox: postbox, peerId: self.peerId)
}
}
func reset(postbox: PostboxImpl) -> Bool {
@ -260,6 +274,53 @@ final class MutablePeerView: MutablePostboxView {
}
}
if self.components.contains(.storyStats) {
var refreshStoryStats = false
for event in transaction.currentStoryTopItemEvents {
if case .replace(peerId: self.peerId) = event {
refreshStoryStats = true
}
}
if !refreshStoryStats {
for event in transaction.storyPeerStatesEvents {
if case .set(.peer(self.peerId)) = event {
refreshStoryStats = true
}
}
}
if refreshStoryStats {
self.storyStats = fetchPeerStoryStats(postbox: postbox, peerId: self.peerId)
}
}
if !transaction.storyPeerStatesEvents.isEmpty || !transaction.currentStoryTopItemEvents.isEmpty {
if let cachedData = self.cachedData {
var updatedPeerIds = Set<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
}
@ -282,6 +343,8 @@ public final class PeerView: PostboxView {
public let media: [MediaId: Media]
public let peerIsContact: Bool
public let groupId: PeerGroupId?
public let storyStats: PeerStoryStats?
public let memberStoryStats: [PeerId: PeerStoryStats]
init(_ mutableView: MutablePeerView) {
self.peerId = mutableView.peerId
@ -293,5 +356,7 @@ public final class PeerView: PostboxView {
self.media = mutableView.media
self.peerIsContact = mutableView.peerIsContact
self.groupId = mutableView.groupId
self.storyStats = mutableView.storyStats
self.memberStoryStats = mutableView.memberStoryStats
}
}

View File

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

View File

@ -44,6 +44,7 @@ public enum PostboxViewKey: Hashable {
case storiesState(key: PostboxStoryStatesKey)
case storyItems(peerId: PeerId)
case storyExpirationTimeItems
case peerStoryStats(peerIds: Set<PeerId>)
public func hash(into hasher: inout Hasher) {
switch self {
@ -147,6 +148,8 @@ public enum PostboxViewKey: Hashable {
hasher.combine(peerId)
case .storyExpirationTimeItems:
hasher.combine(19)
case let .peerStoryStats(peerIds):
hasher.combine(peerIds)
}
}
@ -410,6 +413,12 @@ public enum PostboxViewKey: Hashable {
} else {
return false
}
case let .peerStoryStats(peerIds):
if case .peerStoryStats(peerIds) = rhs {
return true
} else {
return false
}
}
}
}
@ -502,5 +511,7 @@ func postboxViewForKey(postbox: PostboxImpl, key: PostboxViewKey) -> MutablePost
return MutableStoryItemsView(postbox: postbox, peerId: peerId)
case .storyExpirationTimeItems:
return MutableStoryExpirationTimeItemsView(postbox: postbox)
case let .peerStoryStats(peerIds):
return MutablePeerStoryStatsView(postbox: postbox, peerIds: peerIds)
}
}

View File

@ -1490,13 +1490,16 @@ public final class EngineStoryViewListContext {
public final class Item: Equatable {
public let peer: EnginePeer
public let timestamp: Int32
public let storyStats: PeerStoryStats?
public init(
peer: EnginePeer,
timestamp: Int32
timestamp: Int32,
storyStats: PeerStoryStats?
) {
self.peer = peer
self.timestamp = timestamp
self.storyStats = storyStats
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
@ -1506,6 +1509,9 @@ public final class EngineStoryViewListContext {
if lhs.timestamp != rhs.timestamp {
return false
}
if lhs.storyStats != rhs.storyStats {
return false
}
return true
}
}
@ -1545,6 +1551,7 @@ public final class EngineStoryViewListContext {
let storyId: Int32
let disposable = MetaDisposable()
let storyStatsDisposable = MetaDisposable()
var state: InternalState
let statePromise = Promise<InternalState>()
@ -1569,6 +1576,7 @@ public final class EngineStoryViewListContext {
assert(self.queue.isCurrent())
self.disposable.dispose()
self.storyStatsDisposable.dispose()
}
func loadMore() {
@ -1604,8 +1612,9 @@ public final class EngineStoryViewListContext {
for view in views {
switch view {
case let .storyView(userId, date):
if let peer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))) {
items.append(Item(peer: EnginePeer(peer), timestamp: date))
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))
if let peer = transaction.getPeer(peerId) {
items.append(Item(peer: EnginePeer(peer), timestamp: date, storyStats: transaction.getPeerStoryStats(peerId: peerId)))
nextOffset = NextOffset(id: userId, timestamp: date)
}
@ -1702,6 +1711,35 @@ public final class EngineStoryViewListContext {
strongSelf.isLoadingMore = false
strongSelf.statePromise.set(.single(strongSelf.state))
let statsKey: PostboxViewKey = .peerStoryStats(peerIds: Set(strongSelf.state.items.map(\.peer.id)))
strongSelf.storyStatsDisposable.set((strongSelf.account.postbox.combinedView(keys: [statsKey])
|> deliverOn(strongSelf.queue)).start(next: { views in
guard let `self` = self else {
return
}
guard let view = views.views[statsKey] as? PeerStoryStatsView else {
return
}
var updated = false
var items = self.state.items
for i in 0 ..< strongSelf.state.items.count {
let item = items[i]
let value = view.storyStats[item.peer.id]
if item.storyStats != value {
updated = true
items[i] = Item(
peer: item.peer,
timestamp: item.timestamp,
storyStats: value
)
}
}
if updated {
self.state.items = items
self.statePromise.set(.single(self.state))
}
}))
}))
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -2039,6 +2039,12 @@ public final class StoryItemSetContainerComponent: Component {
return
}
self.navigateToPeer(peer: peer, chat: false)
},
openPeerStories: { [weak self] peer, sourceView in
guard let self else {
return
}
self.openPeerStories(peer: peer, sourceView: sourceView)
}
)),
environment: {},
@ -3163,6 +3169,77 @@ public final class StoryItemSetContainerComponent: Component {
}
}
func openPeerStories(peer: EnginePeer, sourceView: UIView) {
guard let component = self.component else {
return
}
let storyContent = StoryContentContextImpl(context: component.context, isHidden: false, focusedPeerId: peer.id, singlePeer: true)
let _ = (storyContent.state
|> filter { $0.slice != nil }
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self, weak sourceView] _ in
guard let self, let component = self.component else {
return
}
var transitionIn: StoryContainerScreen.TransitionIn?
if let sourceView {
transitionIn = StoryContainerScreen.TransitionIn(
sourceView: sourceView,
sourceRect: sourceView.bounds,
sourceCornerRadius: sourceView.bounds.width * 0.5,
sourceIsAvatar: false
)
sourceView.isHidden = true
}
let storyContainerScreen = StoryContainerScreen(
context: component.context,
content: storyContent,
transitionIn: transitionIn,
transitionOut: { peerId, _ in
if let sourceView {
let destinationView = sourceView
return StoryContainerScreen.TransitionOut(
destinationView: destinationView,
transitionView: StoryContainerScreen.TransitionView(
makeView: { [weak destinationView] in
let parentView = UIView()
if let copyView = destinationView?.snapshotContentTree(unhide: true) {
parentView.addSubview(copyView)
}
return parentView
},
updateView: { copyView, state, transition in
guard let view = copyView.subviews.first else {
return
}
let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress)
transition.setPosition(view: view, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
transition.setScale(view: view, scale: size.width / state.destinationSize.width)
},
insertCloneTransitionView: nil
),
destinationRect: destinationView.bounds,
destinationCornerRadius: destinationView.bounds.width * 0.5,
destinationIsAvatar: false,
completed: { [weak sourceView] in
guard let sourceView else {
return
}
sourceView.isHidden = false
}
)
} else {
return nil
}
}
)
component.controller()?.push(storyContainerScreen)
})
}
private func openStoryEditing() {
guard let component = self.component, let peerReference = PeerReference(component.slice.peer._asPeer()) else {
return

View File

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

View File

@ -563,6 +563,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
private var powerSavingMonitoringDisposable: Disposable?
private var avatarNode: ChatAvatarNavigationNode?
private var storyStats: PeerStoryStats?
public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<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
@ -1165,7 +1166,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
var expandAvatar = false
if case let .groupParticipant(storyStats, avatarHeaderNode) = source {
if let storyStats, storyStats.totalCount != 0, let avatarHeaderNode = avatarHeaderNode as? ChatMessageAvatarHeaderNode {
self?.openStories(peerId: peer.id, avatarHeaderNode: avatarHeaderNode)
self?.openStories(peerId: peer.id, avatarHeaderNode: avatarHeaderNode, avatarNode: nil)
return
} else {
expandAvatar = true
@ -4708,8 +4709,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
chatInfoButtonItem = UIBarButtonItem(customDisplayNode: avatarNode)!
self.avatarNode = avatarNode
//avatarNode.updateStoryView(transition: .immediate, theme: self.presentationData.theme)
case .feed:
chatInfoButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
}
@ -4998,6 +4997,24 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
(strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.setPeer(context: strongSelf.context, theme: strongSelf.presentationData.theme, peer: EnginePeer(peer), overrideImage: imageOverride)
(strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.contextActionIsEnabled = strongSelf.chatLocation.threadId == nil && peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil
strongSelf.chatInfoNavigationButton?.buttonItem.accessibilityLabel = presentationInterfaceState.strings.Conversation_ContextMenuOpenProfile
strongSelf.storyStats = peerView.storyStats
if let avatarNode = strongSelf.avatarNode {
avatarNode.avatarNode.setStoryStats(storyStats: peerView.storyStats.flatMap { storyStats -> AvatarNode.StoryStats? in
if storyStats.totalCount == 0 {
return nil
}
return AvatarNode.StoryStats(
totalCount: storyStats.totalCount == 0 ? 0 : 1,
unseenCount: storyStats.unseenCount == 0 ? 0 : 1,
hasUnseenCloseFriendsItems: false
)
}, presentationParams: AvatarNode.StoryPresentationParams(
colors: AvatarNode.Colors(theme: strongSelf.presentationData.theme),
lineWidth: 1.5,
inactiveLineWidth: 1.5
), transition: .immediate)
}
}
}
}
@ -12230,7 +12247,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
@objc func rightNavigationButtonAction() {
if let button = self.rightNavigationButton {
self.navigationButtonAction(button.action)
if case let .peer(peerId) = self.chatLocation, case .openChatInfo(expandAvatar: true) = button.action, let storyStats = self.storyStats, storyStats.totalCount != 0, let avatarNode = self.avatarNode {
self.openStories(peerId: peerId, avatarHeaderNode: nil, avatarNode: avatarNode.avatarNode)
} else {
self.navigationButtonAction(button.action)
}
}
}
@ -17011,12 +17032,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})
}
private func openStories(peerId: EnginePeer.Id, avatarHeaderNode: ChatMessageAvatarHeaderNode) {
private func openStories(peerId: EnginePeer.Id, avatarHeaderNode: ChatMessageAvatarHeaderNode?, avatarNode: AvatarNode?) {
let storyContent = StoryContentContextImpl(context: self.context, isHidden: false, focusedPeerId: peerId, singlePeer: true)
let _ = (storyContent.state
|> filter { $0.slice != nil }
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self, weak avatarHeaderNode] _ in
|> deliverOnMainQueue).start(next: { [weak self, weak avatarHeaderNode, weak avatarNode] _ in
guard let self else {
return
}
@ -17030,6 +17051,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
sourceIsAvatar: false
)
avatarHeaderNode.avatarNode.isHidden = true
} else if let avatarNode {
transitionIn = StoryContainerScreen.TransitionIn(
sourceView: avatarNode.view,
sourceRect: avatarNode.view.bounds,
sourceCornerRadius: avatarNode.view.bounds.width * 0.5,
sourceIsAvatar: false
)
avatarNode.isHidden = true
}
let storyContainerScreen = StoryContainerScreen(
@ -17037,40 +17066,73 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
content: storyContent,
transitionIn: transitionIn,
transitionOut: { peerId, _ in
guard let avatarHeaderNode else {
return nil
}
let destinationView = avatarHeaderNode.avatarNode.view
return StoryContainerScreen.TransitionOut(
destinationView: destinationView,
transitionView: StoryContainerScreen.TransitionView(
makeView: { [weak destinationView] in
let parentView = UIView()
if let copyView = destinationView?.snapshotContentTree(unhide: true) {
parentView.addSubview(copyView)
}
return parentView
},
updateView: { copyView, state, transition in
guard let view = copyView.subviews.first else {
if let avatarHeaderNode {
let destinationView = avatarHeaderNode.avatarNode.view
return StoryContainerScreen.TransitionOut(
destinationView: destinationView,
transitionView: StoryContainerScreen.TransitionView(
makeView: { [weak destinationView] in
let parentView = UIView()
if let copyView = destinationView?.snapshotContentTree(unhide: true) {
parentView.addSubview(copyView)
}
return parentView
},
updateView: { copyView, state, transition in
guard let view = copyView.subviews.first else {
return
}
let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress)
transition.setPosition(view: view, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
transition.setScale(view: view, scale: size.width / state.destinationSize.width)
},
insertCloneTransitionView: nil
),
destinationRect: destinationView.bounds,
destinationCornerRadius: destinationView.bounds.width * 0.5,
destinationIsAvatar: false,
completed: { [weak avatarHeaderNode] in
guard let avatarHeaderNode else {
return
}
let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress)
transition.setPosition(view: view, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
transition.setScale(view: view, scale: size.width / state.destinationSize.width)
},
insertCloneTransitionView: nil
),
destinationRect: destinationView.bounds,
destinationCornerRadius: destinationView.bounds.width * 0.5,
destinationIsAvatar: false,
completed: { [weak avatarHeaderNode] in
guard let avatarHeaderNode else {
return
avatarHeaderNode.avatarNode.isHidden = false
}
avatarHeaderNode.avatarNode.isHidden = false
}
)
)
} else if let avatarNode {
let destinationView = avatarNode.view
return StoryContainerScreen.TransitionOut(
destinationView: destinationView,
transitionView: StoryContainerScreen.TransitionView(
makeView: { [weak destinationView] in
let parentView = UIView()
if let copyView = destinationView?.snapshotContentTree(unhide: true) {
parentView.addSubview(copyView)
}
return parentView
},
updateView: { copyView, state, transition in
guard let view = copyView.subviews.first else {
return
}
let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress)
transition.setPosition(view: view, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
transition.setScale(view: view, scale: size.width / state.destinationSize.width)
},
insertCloneTransitionView: nil
),
destinationRect: destinationView.bounds,
destinationCornerRadius: destinationView.bounds.width * 0.5,
destinationIsAvatar: false,
completed: { [weak avatarNode] in
guard let avatarNode else {
return
}
avatarNode.isHidden = false
}
)
} else {
return nil
}
}
)
self.push(storyContainerScreen)

View File

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

View File

@ -278,15 +278,16 @@ class ChatMessageStoryMentionContentNode: ChatMessageBubbleContentNode {
}
let indicatorFrame = imageFrame
var storyColors = AvatarStoryIndicatorComponent.Colors(theme: item.presentationData.theme.theme)
storyColors.seenColors = [UIColor(white: 1.0, alpha: 0.2), UIColor(white: 1.0, alpha: 0.2)]
let _ = strongSelf.storyIndicator.update(
transition: .immediate,
component: AnyComponent(AvatarStoryIndicatorComponent(
hasUnseen: hasUnseen,
hasUnseenCloseFriendsItems: hasUnseen && (story?.isCloseFriends ?? false),
theme: item.presentationData.theme.theme,
colors: storyColors,
activeLineWidth: 3.0,
inactiveLineWidth: 1.0 + UIScreenPixel,
isGlassBackground: true,
counters: nil
)),
environment: {},

View File

@ -26,6 +26,7 @@ final class PeerInfoScreenMemberItem: PeerInfoScreenItem {
let isAccount: Bool
let action: ((PeerInfoScreenMemberItemAction) -> Void)?
let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
let openStories: ((UIView) -> Void)?
init(
id: AnyHashable,
@ -35,7 +36,8 @@ final class PeerInfoScreenMemberItem: PeerInfoScreenItem {
badge: String? = nil,
isAccount: Bool,
action: ((PeerInfoScreenMemberItemAction) -> Void)?,
contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil
contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil,
openStories: ((UIView) -> Void)? = nil
) {
self.id = id
self.context = context
@ -45,6 +47,7 @@ final class PeerInfoScreenMemberItem: PeerInfoScreenItem {
self.isAccount = isAccount
self.action = action
self.contextAction = contextAction
self.openStories = openStories
}
func node() -> PeerInfoScreenItemNode {
@ -195,7 +198,12 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode {
}, removePeer: { _ in
}, contextAction: item.contextAction, hasTopStripe: false, hasTopGroupInset: false, noInsets: true, noCorners: true, displayDecorations: false)
}, contextAction: item.contextAction, hasTopStripe: false, hasTopGroupInset: false, noInsets: true, noCorners: true, displayDecorations: false, storyStats: item.member.storyStats, openStories: { [weak self] sourceView in
guard let self, let item = self.item else {
return
}
item.openStories?(sourceView)
})
let params = ListViewItemLayoutParams(width: width, leftInset: safeInsets.left, rightInset: safeInsets.right, availableHeight: 1000.0)

View File

@ -26,6 +26,7 @@ enum PeerMembersListAction {
case promote
case restrict
case remove
case openStories(sourceView: UIView)
}
private enum PeerMembersListEntryStableId: Hashable {
@ -35,7 +36,7 @@ private enum PeerMembersListEntryStableId: Hashable {
private enum PeerMembersListEntry: Comparable, Identifiable {
case addMember(PresentationTheme, String)
case member(PresentationTheme, Int, PeerInfoMember)
case member(theme: PresentationTheme, index: Int, member: PeerInfoMember)
var stableId: PeerMembersListEntryStableId {
switch self {
@ -126,7 +127,9 @@ private enum PeerMembersListEntry: Comparable, Identifiable {
action(member, .open)
}, setPeerIdWithRevealedOptions: { _, _ in
}, removePeer: { _ in
}, contextAction: nil, hasTopStripe: false, noInsets: true, noCorners: true, disableInteractiveTransitionIfNecessary: true)
}, contextAction: nil, hasTopStripe: false, noInsets: true, noCorners: true, disableInteractiveTransitionIfNecessary: true, storyStats: member.storyStats, openStories: { sourceView in
action(member, .openStories(sourceView: sourceView))
})
}
}
}
@ -265,7 +268,7 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode {
entries.append(.addMember(presentationData.theme, presentationData.strings.GroupInfo_AddParticipant))
}
for member in state.members {
entries.append(.member(presentationData.theme, entries.count, member))
entries.append(.member(theme: presentationData.theme, index: entries.count, member: member))
}
let transaction = preparedTransition(from: self.currentEntries, to: entries, context: self.context, presentationData: presentationData, enclosingPeer: enclosingPeer, addMemberAction: { [weak self] in

View File

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

View File

@ -379,13 +379,15 @@ final class PeerInfoHeaderNavigationTransition {
let sourceTitleView: ChatTitleView
let sourceTitleFrame: CGRect
let sourceSubtitleFrame: CGRect
let previousAvatarView: UIView?
let fraction: CGFloat
init(sourceNavigationBar: NavigationBar, sourceTitleView: ChatTitleView, sourceTitleFrame: CGRect, sourceSubtitleFrame: CGRect, fraction: CGFloat) {
init(sourceNavigationBar: NavigationBar, sourceTitleView: ChatTitleView, sourceTitleFrame: CGRect, sourceSubtitleFrame: CGRect, previousAvatarView: UIView?, fraction: CGFloat) {
self.sourceNavigationBar = sourceNavigationBar
self.sourceTitleView = sourceTitleView
self.sourceTitleFrame = sourceTitleFrame
self.sourceSubtitleFrame = sourceSubtitleFrame
self.previousAvatarView = previousAvatarView
self.fraction = fraction
}
}
@ -455,40 +457,22 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
}
func updateStoryView(transition: ContainedViewLayoutTransition, theme: PresentationTheme) {
if let storyData = self.storyData {
let avatarStoryView: ComponentView<Empty>
if let current = self.avatarStoryView {
avatarStoryView = current
} else {
avatarStoryView = ComponentView()
self.avatarStoryView = avatarStoryView
}
let _ = avatarStoryView.update(
transition: Transition(transition),
component: AnyComponent(AvatarStoryIndicatorComponent(
hasUnseen: storyData.hasUnseen,
hasUnseenCloseFriendsItems: storyData.hasUnseenCloseFriends,
theme: theme,
activeLineWidth: 3.0,
inactiveLineWidth: 2.0,
counters: nil
)),
environment: {},
containerSize: self.avatarNode.bounds.size
var colors = AvatarNode.Colors(theme: theme)
colors.seenColors = [
theme.list.controlSecondaryColor,
theme.list.controlSecondaryColor
]
self.avatarNode.setStoryStats(storyStats: self.storyData.flatMap { storyData in
return AvatarNode.StoryStats(
totalCount: 1,
unseenCount: storyData.hasUnseen ? 1 : 0,
hasUnseenCloseFriendsItems: storyData.hasUnseenCloseFriends
)
if let avatarStoryComponentView = avatarStoryView.view {
if avatarStoryComponentView.superview == nil {
self.containerNode.view.insertSubview(avatarStoryComponentView, at: 0)
}
avatarStoryComponentView.frame = self.avatarNode.frame
}
} else {
if let avatarStoryView = self.avatarStoryView {
self.avatarStoryView = nil
avatarStoryView.view?.removeFromSuperview()
}
}
}, presentationParams: AvatarNode.StoryPresentationParams(
colors: colors,
lineWidth: 3.0,
inactiveLineWidth: 1.5
), transition: Transition(transition))
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
@ -610,11 +594,11 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
avatarCornerRadius = avatarSize / 2.0
}
if self.avatarNode.layer.cornerRadius != 0.0 {
ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut).updateCornerRadius(layer: self.avatarNode.layer, cornerRadius: avatarCornerRadius)
ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut).updateCornerRadius(layer: self.avatarNode.contentNode.layer, cornerRadius: avatarCornerRadius)
} else {
self.avatarNode.layer.cornerRadius = avatarCornerRadius
self.avatarNode.contentNode.layer.cornerRadius = avatarCornerRadius
}
self.avatarNode.layer.masksToBounds = true
self.avatarNode.contentNode.layer.masksToBounds = true
self.isFirstAvatarLoading = false
@ -1175,7 +1159,6 @@ final class PeerInfoAvatarListNode: ASDisplayNode {
self.containerNode = ASDisplayNode()
self.bottomCoverNode = ASDisplayNode()
self.bottomCoverNode.backgroundColor = .black
self.maskNode = DynamicIslandMaskNode()
self.pinchSourceNode = PinchSourceContainerNode()
@ -2918,6 +2901,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
} else {
transitionSourceAvatarFrame = avatarNavigationNode.avatarNode.view.convert(avatarNavigationNode.avatarNode.view.bounds, to: navigationTransition.sourceNavigationBar.view)
}
transition.updateAlpha(node: self.avatarListNode.avatarContainerNode.avatarNode, alpha: 1.0 - transitionFraction)
} else {
if deviceMetrics.hasDynamicIsland && !isLandscape {
transitionSourceAvatarFrame = CGRect(origin: CGPoint(x: avatarFrame.minX, y: -20.0), size: avatarFrame.size).insetBy(dx: avatarSize * 0.4, dy: avatarSize * 0.4)
@ -3370,7 +3354,13 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let avatarOffset: CGFloat
if self.navigationTransition != nil {
if let transitionSourceAvatarFrame = transitionSourceAvatarFrame {
avatarScale = ((1.0 - transitionFraction) * avatarFrame.width + transitionFraction * transitionSourceAvatarFrame.width) / avatarFrame.width
var trueAvatarSize = transitionSourceAvatarFrame.size
if self.avatarListNode.avatarContainerNode.avatarNode.storyStats != nil {
trueAvatarSize.width -= 1.33 * 4.0
trueAvatarSize.height -= 1.33 * 4.0
}
avatarScale = ((1.0 - transitionFraction) * avatarFrame.width + transitionFraction * trueAvatarSize.width) / avatarFrame.width
} else {
avatarScale = 1.0
}
@ -3380,6 +3370,14 @@ final class PeerInfoHeaderNode: ASDisplayNode {
avatarOffset = apparentTitleLockOffset + 0.0 * (1.0 - titleCollapseFraction) + 10.0 * titleCollapseFraction
}
if let previousAvatarView = self.navigationTransition?.previousAvatarView, let transitionSourceAvatarFrame {
let previousScale = ((1.0 - transitionFraction) * avatarFrame.width + transitionFraction * transitionSourceAvatarFrame.width) / transitionSourceAvatarFrame.width
transition.updateAlpha(layer: previousAvatarView.layer, alpha: transitionFraction)
transition.updateTransformScale(layer: previousAvatarView.layer, scale: previousScale)
transition.updatePosition(layer: previousAvatarView.layer, position: self.view.convert(CGPoint(x: avatarCenter.x - (27.0 * (1.0 - transitionFraction) + 10 * transitionFraction), y: avatarCenter.y - (2.66 * (1.0 - transitionFraction) + 1.0 * transitionFraction)), to: previousAvatarView.superview))
}
if subtitleIsButton {
subtitleFrame.origin.y += 11.0 * (1.0 - titleCollapseFraction)
if let subtitleBackgroundButton = self.subtitleBackgroundButton {
@ -3398,8 +3396,14 @@ final class PeerInfoHeaderNode: ASDisplayNode {
if self.isAvatarExpanded {
self.avatarListNode.listContainerNode.isHidden = false
if let transitionSourceAvatarFrame = transitionSourceAvatarFrame {
transition.updateCornerRadius(node: self.avatarListNode.listContainerNode, cornerRadius: transitionFraction * transitionSourceAvatarFrame.width / 2.0)
transition.updateCornerRadius(node: self.avatarListNode.listContainerNode.controlsClippingNode, cornerRadius: transitionFraction * transitionSourceAvatarFrame.width / 2.0)
var trueAvatarSize = transitionSourceAvatarFrame.size
if self.avatarListNode.avatarContainerNode.avatarNode.storyStats != nil {
trueAvatarSize.width -= 1.33 * 4.0
trueAvatarSize.height -= 1.33 * 4.0
}
transition.updateCornerRadius(node: self.avatarListNode.listContainerNode, cornerRadius: transitionFraction * trueAvatarSize.width / 2.0)
transition.updateCornerRadius(node: self.avatarListNode.listContainerNode.controlsClippingNode, cornerRadius: transitionFraction * trueAvatarSize.width / 2.0)
} else {
transition.updateCornerRadius(node: self.avatarListNode.listContainerNode, cornerRadius: 0.0)
transition.updateCornerRadius(node: self.avatarListNode.listContainerNode.controlsClippingNode, cornerRadius: 0.0)
@ -3435,19 +3439,31 @@ final class PeerInfoHeaderNode: ASDisplayNode {
transition.updateAlpha(layer: avatarStoryView.layer, alpha: 1.0 - transitionFraction)
}
let apparentAvatarFrame: CGRect
var apparentAvatarFrame: CGRect
let controlsClippingFrame: CGRect
if self.isAvatarExpanded {
let expandedAvatarCenter = CGPoint(x: expandedAvatarListSize.width / 2.0, y: expandedAvatarListSize.height / 2.0 - contentOffset / 2.0)
apparentAvatarFrame = CGRect(origin: CGPoint(x: expandedAvatarCenter.x * (1.0 - transitionFraction) + transitionFraction * avatarCenter.x, y: expandedAvatarCenter.y * (1.0 - transitionFraction) + transitionFraction * avatarCenter.y), size: CGSize())
if let transitionSourceAvatarFrame = transitionSourceAvatarFrame {
var trueAvatarSize = transitionSourceAvatarFrame.size
if self.avatarListNode.avatarContainerNode.avatarNode.storyStats != nil {
trueAvatarSize.width -= 1.33 * 4.0
trueAvatarSize.height -= 1.33 * 4.0
}
let trueAvatarFrame = trueAvatarSize.centered(around: transitionSourceAvatarFrame.center)
let expandedFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: expandedAvatarListSize)
controlsClippingFrame = CGRect(origin: CGPoint(x: transitionFraction * transitionSourceAvatarFrame.minX + (1.0 - transitionFraction) * expandedFrame.minX, y: transitionFraction * transitionSourceAvatarFrame.minY + (1.0 - transitionFraction) * expandedFrame.minY), size: CGSize(width: transitionFraction * transitionSourceAvatarFrame.width + (1.0 - transitionFraction) * expandedFrame.width, height: transitionFraction * transitionSourceAvatarFrame.height + (1.0 - transitionFraction) * expandedFrame.height))
controlsClippingFrame = CGRect(origin: CGPoint(x: transitionFraction * trueAvatarFrame.minX + (1.0 - transitionFraction) * expandedFrame.minX, y: transitionFraction * trueAvatarFrame.minY + (1.0 - transitionFraction) * expandedFrame.minY), size: CGSize(width: transitionFraction * trueAvatarFrame.width + (1.0 - transitionFraction) * expandedFrame.width, height: transitionFraction * trueAvatarFrame.height + (1.0 - transitionFraction) * expandedFrame.height))
} else {
controlsClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: expandedAvatarListSize)
}
} else {
apparentAvatarFrame = CGRect(origin: CGPoint(x: avatarCenter.x - avatarFrame.width / 2.0, y: -contentOffset + avatarOffset + avatarCenter.y - avatarFrame.height / 2.0), size: avatarFrame.size)
var trueAvatarSize = avatarFrame.size
if self.avatarListNode.avatarContainerNode.avatarNode.storyStats != nil {
trueAvatarSize.width -= 3.0 * 4.0
trueAvatarSize.height -= 3.0 * 4.0
}
apparentAvatarFrame = CGRect(origin: CGPoint(x: avatarCenter.x - trueAvatarSize.width / 2.0, y: -contentOffset + avatarOffset + avatarCenter.y - trueAvatarSize.height / 2.0), size: trueAvatarSize)
controlsClippingFrame = apparentAvatarFrame
}
@ -3466,7 +3482,13 @@ final class PeerInfoHeaderNode: ASDisplayNode {
if self.isAvatarExpanded {
if let transitionSourceAvatarFrame = transitionSourceAvatarFrame {
let neutralAvatarListContainerSize = expandedAvatarListSize
let avatarListContainerSize = CGSize(width: neutralAvatarListContainerSize.width * (1.0 - transitionFraction) + transitionSourceAvatarFrame.width * transitionFraction, height: neutralAvatarListContainerSize.height * (1.0 - transitionFraction) + transitionSourceAvatarFrame.height * transitionFraction)
var avatarListContainerSize = CGSize(width: neutralAvatarListContainerSize.width * (1.0 - transitionFraction) + transitionSourceAvatarFrame.width * transitionFraction, height: neutralAvatarListContainerSize.height * (1.0 - transitionFraction) + transitionSourceAvatarFrame.height * transitionFraction)
if self.avatarListNode.avatarContainerNode.avatarNode.storyStats != nil {
avatarListContainerSize.width -= 1.33 * 5.0
avatarListContainerSize.height -= 1.33 * 5.0
}
avatarListContainerFrame = CGRect(origin: CGPoint(x: -avatarListContainerSize.width / 2.0, y: -avatarListContainerSize.height / 2.0), size: avatarListContainerSize)
} else {
avatarListContainerFrame = CGRect(origin: CGPoint(x: -expandedAvatarListSize.width / 2.0, y: -expandedAvatarListSize.height / 2.0), size: expandedAvatarListSize)
@ -3512,6 +3534,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
}
self.avatarListNode.topCoverNode.update(maskValue)
self.avatarListNode.maskNode.update(maskValue)
self.avatarListNode.bottomCoverNode.backgroundColor = UIColor(white: 0.0, alpha: maskValue)
self.avatarListNode.listContainerNode.topShadowNode.isHidden = !self.isAvatarExpanded
@ -3520,7 +3543,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
avatarMaskOffset -= contentOffset
}
self.avatarListNode.maskNode.position = CGPoint(x: 0.0, y: -self.avatarListNode.frame.minY + 48.0 + 85.5 + avatarMaskOffset)
self.avatarListNode.maskNode.position = CGPoint(x: 0.0, y: -self.avatarListNode.frame.minY + 48.0 + 85.0 + avatarMaskOffset)
self.avatarListNode.maskNode.bounds = CGRect(origin: .zero, size: CGSize(width: 171.0, height: 171.0))
self.avatarListNode.bottomCoverNode.position = self.avatarListNode.maskNode.position

View File

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

View File

@ -448,6 +448,7 @@ private enum PeerInfoMemberAction {
case promote
case restrict
case remove
case openStories(sourceView: UIView)
}
private enum PeerInfoContextSubject {
@ -1389,6 +1390,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
case .remove:
interaction.performMemberAction(member, .remove)
}
}, openStories: { sourceView in
interaction.performMemberAction(member, .openStories(sourceView: sourceView))
}))
}
}
@ -3012,6 +3015,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
strongSelf.performMemberAction(member: member, action: .restrict)
case .remove:
strongSelf.performMemberAction(member: member, action: .remove)
case let .openStories(sourceView):
strongSelf.performMemberAction(member: member, action: .openStories(sourceView: sourceView))
}
}
@ -7099,7 +7104,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
switch action {
case .promote:
if case let .channelMember(channelMember) = member {
if case let .channelMember(channelMember, _) = member {
var upgradedToSupergroupImpl: (() -> Void)?
let controller = channelAdminController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id, adminId: member.id, initialParticipant: channelMember.participant, updated: { _ in
}, upgradedToSupergroup: { _, f in
@ -7116,7 +7121,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
}
case .restrict:
if case let .channelMember(channelMember) = member {
if case let .channelMember(channelMember, _) = member {
var upgradedToSupergroupImpl: (() -> Void)?
let controller = channelBannedMemberController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id, memberId: member.id, initialParticipant: channelMember.participant, updated: { _ in
@ -7135,6 +7140,71 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
case .remove:
data.members?.membersContext.removeMember(memberId: member.id)
case let .openStories(sourceView):
let storyContent = StoryContentContextImpl(context: self.context, isHidden: false, focusedPeerId: member.id, singlePeer: true)
let _ = (storyContent.state
|> filter { $0.slice != nil }
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self, weak sourceView] _ in
guard let self else {
return
}
var transitionIn: StoryContainerScreen.TransitionIn?
if let sourceView {
transitionIn = StoryContainerScreen.TransitionIn(
sourceView: sourceView,
sourceRect: sourceView.bounds,
sourceCornerRadius: sourceView.bounds.width * 0.5,
sourceIsAvatar: false
)
sourceView.isHidden = true
}
let storyContainerScreen = StoryContainerScreen(
context: self.context,
content: storyContent,
transitionIn: transitionIn,
transitionOut: { peerId, _ in
if let sourceView {
let destinationView = sourceView
return StoryContainerScreen.TransitionOut(
destinationView: destinationView,
transitionView: StoryContainerScreen.TransitionView(
makeView: { [weak destinationView] in
let parentView = UIView()
if let copyView = destinationView?.snapshotContentTree(unhide: true) {
parentView.addSubview(copyView)
}
return parentView
},
updateView: { copyView, state, transition in
guard let view = copyView.subviews.first else {
return
}
let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress)
transition.setPosition(view: view, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
transition.setScale(view: view, scale: size.width / state.destinationSize.width)
},
insertCloneTransitionView: nil
),
destinationRect: destinationView.bounds,
destinationCornerRadius: destinationView.bounds.width * 0.5,
destinationIsAvatar: false,
completed: { [weak sourceView] in
guard let sourceView else {
return
}
sourceView.isHidden = false
}
)
} else {
return nil
}
}
)
self.controller?.push(storyContainerScreen)
})
}
}
@ -10503,6 +10573,8 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig
private var previousTitleNode: (ASDisplayNode, PortalView)?
private var previousStatusNode: (ASDisplayNode, ASDisplayNode)?
private var previousAvatarView: UIView?
private var didSetup: Bool = false
init(screenNode: PeerInfoScreenNode, presentationData: PresentationData, headerNode: PeerInfoHeaderNode) {
@ -10576,7 +10648,9 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig
self.view.layer.addSublayer(previousRightButton)
}
} else {
if let _ = bottomNavigationBar.rightButtonNode.singleCustomNode as? ChatAvatarNavigationNode {
if let avatarNavigationNode = bottomNavigationBar.rightButtonNode.singleCustomNode as? ChatAvatarNavigationNode, let previousAvatarView = avatarNavigationNode.view.snapshotContentTree() {
self.previousAvatarView = previousAvatarView
self.view.addSubview(previousAvatarView)
} else if let previousRightButton = bottomNavigationBar.rightButtonNode.view.layer.snapshotContentTree() {
self.previousRightButton = previousRightButton
self.view.layer.addSublayer(previousRightButton)
@ -10706,7 +10780,7 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig
let previousTitleFrame = previousTitleView.titleContainerView.convert(previousTitleView.titleContainerView.bounds, to: bottomNavigationBar.view)
let previousStatusFrame = previousTitleView.activityNode.view.convert(previousTitleView.activityNode.bounds, to: bottomNavigationBar.view)
self.headerNode.navigationTransition = PeerInfoHeaderNavigationTransition(sourceNavigationBar: bottomNavigationBar, sourceTitleView: previousTitleView, sourceTitleFrame: previousTitleFrame, sourceSubtitleFrame: previousStatusFrame, fraction: fraction)
self.headerNode.navigationTransition = PeerInfoHeaderNavigationTransition(sourceNavigationBar: bottomNavigationBar, sourceTitleView: previousTitleView, sourceTitleFrame: previousTitleFrame, sourceSubtitleFrame: previousStatusFrame, previousAvatarView: self.previousAvatarView, fraction: fraction)
var topHeight = topNavigationBar.backgroundNode.bounds.height
if let iconView = previousTitleView.titleCredibilityIconView.componentView {

View File

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

View File

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