mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
Stories
This commit is contained in:
parent
2575b0e2f1
commit
008b52a250
@ -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 {
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
76
submodules/Postbox/Sources/PeerStoryStatsView.swift
Normal file
76
submodules/Postbox/Sources/PeerStoryStatsView.swift
Normal 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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
||||
|
@ -17,6 +17,7 @@ swift_library(
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/CheckNode",
|
||||
|
@ -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())
|
||||
|
@ -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
|
||||
|
@ -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: {},
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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: {},
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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]))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user