This commit is contained in:
Ali 2023-07-07 18:55:23 +04:00
parent 9d8777443f
commit a6d7f6d45d
13 changed files with 804 additions and 454 deletions

View File

@ -20,6 +20,8 @@ swift_library(
"//submodules/Emoji:Emoji",
"//submodules/TinyThumbnail:TinyThumbnail",
"//submodules/FastBlur:FastBlur",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent",
],
visibility = [
"//visibility:public",

View File

@ -10,6 +10,8 @@ import AppBundle
import AccountContext
import Emoji
import Accelerate
import ComponentFlow
import AvatarStoryIndicatorComponent
private let deletedIcon = UIImage(bundleImageName: "Avatar/DeletedIcon")?.precomposed()
private let phoneIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/PhoneIcon"), color: .white)
@ -224,6 +226,7 @@ public final class AvatarNode: ASDisplayNode {
UIColor(rgb: 0x2a9ef1), UIColor(rgb: 0x72d5fd)
]
public final class ContentNode: ASDisplayNode {
public var font: UIFont {
didSet {
if oldValue.pointSize != font.pointSize {
@ -246,7 +249,7 @@ public final class AvatarNode: ASDisplayNode {
public var editOverlayNode: AvatarEditOverlayNode?
private let imageReadyDisposable = MetaDisposable()
private var state: AvatarNodeState = .empty
fileprivate var state: AvatarNodeState = .empty
public var unroundedImage: UIImage?
private var currentImage: UIImage?
@ -322,19 +325,6 @@ public final class AvatarNode: ASDisplayNode {
}
}
override public var frame: CGRect {
get {
return super.frame
} set(value) {
let updateImage = !value.size.equalTo(super.frame.size)
super.frame = value
if updateImage {
self.updateSize(size: value.size)
}
}
}
public func updateSize(size: CGSize) {
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
self.editOverlayNode?.frame = self.imageNode.frame
@ -672,29 +662,238 @@ public final class AvatarNode: ASDisplayNode {
}
}
}
}
static func asyncLayout(_ node: AvatarNode?) -> (_ context: AccountContext, _ peer: EnginePeer, _ font: UIFont) -> () -> AvatarNode? {
let currentState = node?.state
let createNode = node == nil
return { [weak node] context, peer, font in
let state: AvatarNodeState = .peerAvatar(peer.id, peer.displayLetters, peer.smallProfileImage, .round)
if currentState != state {
public let contentNode: ContentNode
private var storyIndicatorTheme: PresentationTheme?
private var storyIndicator: ComponentView<Empty>?
public struct StoryStats: Equatable {
public var totalCount: Int
public var unseenCount: Int
public var hasUnseenCloseFriendsItems: Bool
public init(
totalCount: Int,
unseenCount: Int,
hasUnseenCloseFriendsItems: Bool
) {
self.totalCount = totalCount
self.unseenCount = unseenCount
self.hasUnseenCloseFriendsItems = hasUnseenCloseFriendsItems
}
var createdNode: AvatarNode?
if createNode {
createdNode = AvatarNode(font: font)
}
return {
let updatedNode: AvatarNode?
if let createdNode = createdNode {
updatedNode = createdNode
public private(set) var storyStats: StoryStats?
public var font: UIFont {
get {
return self.contentNode.font
} set(value) {
self.contentNode.font = value
}
}
public var editOverlayNode: AvatarEditOverlayNode? {
get {
return self.contentNode.editOverlayNode
} set(value) {
self.contentNode.editOverlayNode = value
}
}
public var unroundedImage: UIImage? {
get {
return self.contentNode.unroundedImage
} set(value) {
self.contentNode.unroundedImage = value
}
}
public var badgeView: AvatarBadgeView? {
get {
return self.contentNode.badgeView
} set(value) {
self.contentNode.badgeView = value
}
}
public var ready: Signal<Void, NoError> {
return self.contentNode.ready
}
public var imageNode: ImageNode {
return self.contentNode.imageNode
}
public init(font: UIFont) {
self.contentNode = ContentNode(font: font)
super.init()
self.onDidLoad { [weak self] _ in
guard let self else {
return
}
if let storyIndicatorTheme = self.storyIndicatorTheme {
self.updateStoryIndicator(theme: storyIndicatorTheme, transition: .immediate)
}
}
self.addSubnode(self.contentNode)
}
override public var frame: CGRect {
get {
return super.frame
} set(value) {
let updateImage = !value.size.equalTo(super.frame.size)
super.frame = value
if updateImage {
self.updateSize(size: value.size)
}
}
}
override public func nodeDidLoad() {
super.nodeDidLoad()
}
public func updateSize(size: CGSize) {
self.contentNode.position = CGRect(origin: CGPoint(), size: size).center
self.contentNode.bounds = CGRect(origin: CGPoint(), size: size)
self.contentNode.updateSize(size: size)
if let storyIndicatorTheme = self.storyIndicatorTheme {
self.updateStoryIndicator(theme: storyIndicatorTheme, transition: .immediate)
}
}
public func playArchiveAnimation() {
self.contentNode.playArchiveAnimation()
}
public func setPeer(
context: AccountContext,
account: Account? = nil,
theme: PresentationTheme,
peer: EnginePeer?,
authorOfMessage: MessageReference? = nil,
overrideImage: AvatarNodeImageOverride? = nil,
emptyColor: UIColor? = nil,
clipStyle: AvatarNodeClipStyle = .round,
synchronousLoad: Bool = false,
displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0),
storeUnrounded: Bool = false
) {
self.contentNode.setPeer(
context: context,
account: account,
theme: theme,
peer: peer,
authorOfMessage: authorOfMessage,
overrideImage: overrideImage,
emptyColor: emptyColor,
clipStyle: clipStyle,
synchronousLoad: synchronousLoad,
displayDimensions: displayDimensions,
storeUnrounded: storeUnrounded
)
}
public func setCustomLetters(_ letters: [String], explicitColor: AvatarNodeColorOverride? = nil, icon: AvatarNodeExplicitIcon? = nil) {
self.contentNode.setCustomLetters(letters, explicitColor: explicitColor, icon: icon)
}
public func setStoryStats(storyStats: StoryStats?, theme: PresentationTheme, transition: Transition) {
if self.storyStats != storyStats || self.storyIndicatorTheme !== theme {
self.storyStats = storyStats
self.storyIndicatorTheme = theme
self.updateStoryIndicator(theme: theme, transition: transition)
}
}
private struct StoryIndicatorParams {
let lineWidth: CGFloat
let indicatorSize: CGSize
let avatarScale: CGFloat
init(lineWidth: CGFloat, indicatorSize: CGSize, avatarScale: CGFloat) {
self.lineWidth = lineWidth
self.indicatorSize = indicatorSize
self.avatarScale = avatarScale
}
}
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) {
if !self.isNodeLoaded {
return
}
if self.bounds.isEmpty {
return
}
self.storyIndicatorTheme = theme
let size = self.bounds.size
if let storyStats = self.storyStats {
let indicatorParams = self.storyIndicatorParams(size: size)
let storyIndicator: ComponentView<Empty>
var indicatorTransition = transition
if let current = self.storyIndicator {
storyIndicator = current
} else {
updatedNode = node
indicatorTransition = transition.withAnimation(.none)
storyIndicator = ComponentView()
self.storyIndicator = storyIndicator
}
if let updatedNode = updatedNode {
return updatedNode
let _ = storyIndicator.update(
transition: indicatorTransition,
component: AnyComponent(AvatarStoryIndicatorComponent(
hasUnseen: storyStats.unseenCount != 0,
hasUnseenCloseFriendsItems: storyStats.hasUnseenCloseFriendsItems,
theme: theme,
activeLineWidth: indicatorParams.lineWidth,
inactiveLineWidth: indicatorParams.lineWidth,
isGlassBackground: false,
counters: AvatarStoryIndicatorComponent.Counters(
totalCount: storyStats.totalCount,
unseenCount: storyStats.unseenCount
)
)),
environment: {},
containerSize: indicatorParams.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))
}
transition.setScale(view: self.contentNode.view, scale: indicatorParams.avatarScale)
} else {
return nil
transition.setScale(view: self.contentNode.view, scale: 1.0)
if let storyIndicator = self.storyIndicator {
self.storyIndicator = nil
if let storyIndicatorView = storyIndicator.view {
transition.setAlpha(view: storyIndicatorView, alpha: 0.0, completion: { [weak storyIndicatorView] _ in
storyIndicatorView?.removeFromSuperview()
})
}
}
}

View File

@ -327,6 +327,8 @@ final class MutableMessageHistoryView {
private var userId: Int64?
fileprivate var peerStoryStats: [PeerId: PeerStoryStats] = [:]
init(
postbox: PostboxImpl,
orderStatistics: MessageHistoryViewOrderStatistics,
@ -388,6 +390,7 @@ final class MutableMessageHistoryView {
self.sampledState = self.state.sample(postbox: postbox, clipHoles: self.clipHoles)
self.render(postbox: postbox)
let _ = self.updateStoryStats(postbox: postbox)
}
private func reset(postbox: PostboxImpl) {
@ -411,6 +414,8 @@ final class MutableMessageHistoryView {
}
}
self.sampledState = self.state.sample(postbox: postbox, clipHoles: self.clipHoles)
let _ = self.updateStoryStats(postbox: postbox)
}
func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool {
@ -907,6 +912,11 @@ final class MutableMessageHistoryView {
if hasChanges {
self.render(postbox: postbox)
}
if hasChanges || !transaction.currentStoryTopItemEvents.isEmpty || !transaction.storyPeerStatesEvents.isEmpty {
if self.updateStoryStats(postbox: postbox) {
hasChanges = true
}
}
return hasChanges
}
@ -920,6 +930,26 @@ final class MutableMessageHistoryView {
}
}
private func updateStoryStats(postbox: PostboxImpl) -> Bool {
//TODO:optimize refresh
var peerStoryStats: [PeerId: PeerStoryStats] = [:]
if case let .loaded(state) = self.sampledState {
for entry in state.entries {
if let author = entry.message.author {
if let value = fetchPeerStoryStats(postbox: postbox, peerId: author.id) {
peerStoryStats[author.id] = value
}
}
}
}
if self.peerStoryStats != peerStoryStats {
self.peerStoryStats = peerStoryStats
return true
} else {
return false
}
}
func firstHole() -> (MessageHistoryViewHole, MessageHistoryViewRelativeHoleDirection, Int, Int64?)? {
switch self.sampledState {
case let .loading(loadingSample):
@ -965,6 +995,7 @@ public final class MessageHistoryView {
public let isLoading: Bool
public let isLoadingEarlier: Bool
public let isAddedToChatList: Bool
public let peerStoryStats: [PeerId: PeerStoryStats]
public init(tagMask: MessageTags?, namespaces: MessageIdNamespaces, entries: [MessageHistoryEntry], holeEarlier: Bool, holeLater: Bool, isLoading: Bool) {
self.tagMask = tagMask
@ -983,6 +1014,7 @@ public final class MessageHistoryView {
self.isLoading = isLoading
self.isLoadingEarlier = true
self.isAddedToChatList = false
self.peerStoryStats = [:]
}
init(_ mutableView: MutableMessageHistoryView) {
@ -1207,6 +1239,7 @@ public final class MessageHistoryView {
}
self.entries = entries
self.peerStoryStats = mutableView.peerStoryStats
}
public init(base: MessageHistoryView, fixed combinedReadStates: MessageHistoryViewReadState?, transient transientReadStates: MessageHistoryViewReadState?) {
@ -1225,6 +1258,7 @@ public final class MessageHistoryView {
self.isLoading = base.isLoading
self.isLoadingEarlier = base.isLoadingEarlier
self.isAddedToChatList = base.isAddedToChatList
self.peerStoryStats = base.peerStoryStats
if let combinedReadStates = combinedReadStates {
switch combinedReadStates {

View File

@ -50,11 +50,10 @@ final class StoryTopItemsTable: Table {
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: false)
}
private let sharedKey = ValueBoxKey(length: 8 + 4)
private func key(_ key: Key) -> ValueBoxKey {
self.sharedKey.setInt64(0, value: key.peerId.toInt64())
return self.sharedKey
let keyValue = ValueBoxKey(length: 8)
keyValue.setInt64(0, value: key.peerId.toInt64())
return keyValue
}
public func get(peerId: PeerId) -> Entry? {
@ -115,12 +114,11 @@ final class StoryItemsTable: Table {
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: false)
}
private let sharedKey = ValueBoxKey(length: 8 + 4)
private func key(_ key: Key) -> ValueBoxKey {
self.sharedKey.setInt64(0, value: key.peerId.toInt64())
self.sharedKey.setInt32(8, value: key.id)
return self.sharedKey
let keyValue = ValueBoxKey(length: 8 + 4)
keyValue.setInt64(0, value: key.peerId.toInt64())
keyValue.setInt32(8, value: key.id)
return keyValue
}
private func lowerBound(peerId: PeerId) -> ValueBoxKey {
@ -159,6 +157,8 @@ final class StoryItemsTable: Table {
self.valueBox.range(self.table, start: self.lowerBound(peerId: peerId), end: self.upperBound(peerId: peerId), values: { key, value in
let id = key.getInt32(8)
assert(peerId.toInt64() == key.getInt64(0))
let entry: CodableEntry
var expirationTimestamp: Int32?

View File

@ -1127,7 +1127,7 @@ func _internal_markStoryAsSeen(account: Account, peerId: PeerId, id: Int32, asPi
return .complete()
}
#if DEBUG && true
#if DEBUG && false
if "".isEmpty {
return .complete()
}
@ -1156,7 +1156,7 @@ func _internal_markStoryAsSeen(account: Account, peerId: PeerId, id: Int32, asPi
account.stateManager.injectStoryUpdates(updates: [.read(peerId: peerId, maxId: id)])
#if DEBUG && false
#if DEBUG && true
if "".isEmpty {
return .complete()
}

View File

@ -79,7 +79,7 @@ public final class ChatControllerInteraction {
public enum OpenPeerSource {
case `default`
case reaction
case groupParticipant
case groupParticipant(storyStats: PeerStoryStats?, avatarHeaderNode: ASDisplayNode?)
}
public let openMessage: (Message, ChatControllerInteractionOpenMessageMode) -> Bool

View File

@ -1426,7 +1426,8 @@ public final class StoryItemSetContainerComponent: Component {
transitionViewImpl.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
}
leftInfoView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
contentContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.controlsContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
for transitionViewImpl in transitionViewsImpl {
transition.setFrame(view: transitionViewImpl, frame: sourceLocalFrame)

View File

@ -56,7 +56,7 @@ private func calculateCircleIntersection(center: CGPoint, otherCenter: CGPoint,
return (point1Angle, point2Angle)
}
private func calculateMergingCircleShape(center: CGPoint, leftCenter: CGPoint?, rightCenter: CGPoint?, radius: CGFloat, totalCount: Int, unseenCount: Int, isSeen: Bool) -> CGPath {
private func calculateMergingCircleShape(center: CGPoint, leftCenter: CGPoint?, rightCenter: CGPoint?, radius: CGFloat, totalCount: Int, unseenCount: Int, isSeen: Bool, segmentFraction: CGFloat) -> CGPath {
let leftAngles = leftCenter.flatMap { calculateCircleIntersection(center: center, otherCenter: $0, radius: radius) }
let rightAngles = rightCenter.flatMap { calculateCircleIntersection(center: center, otherCenter: $0, radius: radius) }
@ -95,7 +95,7 @@ private func calculateMergingCircleShape(center: CGPoint, leftCenter: CGPoint?,
}
}
} else {
let segmentSpacing: CGFloat = 4.0
let segmentSpacing: CGFloat = 4.0 * segmentFraction
let segmentSpacingAngle: CGFloat = segmentSpacing / radius
let segmentAngle = (2.0 * CGFloat.pi - segmentSpacingAngle * CGFloat(segmentCount)) / CGFloat(segmentCount)
for i in 0 ..< segmentCount {
@ -111,7 +111,9 @@ private func calculateMergingCircleShape(center: CGPoint, leftCenter: CGPoint?,
}
}
let startAngle = segmentSpacingAngle * 0.5 - CGFloat.pi * 0.5 + CGFloat(i) * (segmentSpacingAngle + segmentAngle)
var startAngle = segmentSpacingAngle * 0.5 - CGFloat.pi * 0.5 + CGFloat(i) * (segmentSpacingAngle + segmentAngle)
startAngle += (1.0 - segmentFraction) * CGFloat.pi * 2.0 * 0.25
let endAngle = startAngle + segmentAngle
path.move(to: CGPoint(x: center.x + cos(startAngle) * radius, y: center.y + sin(startAngle) * radius))
path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false)
@ -765,8 +767,8 @@ public final class StoryPeerListItemComponent: Component {
}
Transition.immediate.setShapeLayerPath(layer: self.avatarShapeLayer, path: avatarPath)
Transition.immediate.setShapeLayerPath(layer: self.indicatorShapeSeenLayer, path: calculateMergingCircleShape(center: indicatorCenter, leftCenter: mappedLeftCenter, rightCenter: mappedRightCenter, radius: indicatorRadius - indicatorLineUnseenWidth * 0.5, totalCount: component.totalCount, unseenCount: component.unseenCount, isSeen: true))
Transition.immediate.setShapeLayerPath(layer: self.indicatorShapeUnseenLayer, path: calculateMergingCircleShape(center: indicatorCenter, leftCenter: mappedLeftCenter, rightCenter: mappedRightCenter, radius: indicatorRadius - indicatorLineUnseenWidth * 0.5, totalCount: component.totalCount, unseenCount: component.unseenCount, isSeen: false))
Transition.immediate.setShapeLayerPath(layer: self.indicatorShapeSeenLayer, path: calculateMergingCircleShape(center: indicatorCenter, leftCenter: mappedLeftCenter, rightCenter: mappedRightCenter, radius: indicatorRadius - indicatorLineUnseenWidth * 0.5, totalCount: component.totalCount, unseenCount: component.unseenCount, isSeen: true, segmentFraction: component.expandedAlphaFraction))
Transition.immediate.setShapeLayerPath(layer: self.indicatorShapeUnseenLayer, path: calculateMergingCircleShape(center: indicatorCenter, leftCenter: mappedLeftCenter, rightCenter: mappedRightCenter, radius: indicatorRadius - indicatorLineUnseenWidth * 0.5, totalCount: component.totalCount, unseenCount: component.unseenCount, isSeen: false, segmentFraction: component.expandedAlphaFraction))
//TODO:localize
let titleString: String
@ -810,7 +812,7 @@ public final class StoryPeerListItemComponent: Component {
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: titleString, font: Font.regular(11.0), textColor: component.theme.list.itemPrimaryTextColor)),
text: .plain(NSAttributedString(string: titleString, font: Font.regular(11.0), textColor: (component.unseenCount != 0 || component.peer.id == component.context.account.peerId) ? component.theme.list.itemPrimaryTextColor : component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.5))),
maximumNumberOfLines: 1
)),
environment: {},
@ -840,7 +842,7 @@ public final class StoryPeerListItemComponent: Component {
self.progressLayer = progressLayer
self.indicatorMaskUnseenLayer.addSublayer(progressLayer)
}
let progressFrame = CGRect(origin: CGPoint(), size: indicatorFrame.size).insetBy(dx: 2.0, dy: 2.0)
let progressFrame = CGRect(origin: CGPoint(), size: indicatorFrame.size).insetBy(dx: 4.0, dy: 4.0)
progressTransition.setFrame(layer: progressLayer, frame: progressFrame)
switch ringAnimation {
@ -851,9 +853,9 @@ public final class StoryPeerListItemComponent: Component {
} else {
progressTransition = .easeInOut(duration: 0.3)
}
progressLayer.update(size: progressFrame.size, lineWidth: 4.0, value: .progress(progress), transition: progressTransition)
progressLayer.update(size: progressFrame.size, lineWidth: indicatorLineUnseenWidth, value: .progress(progress), transition: progressTransition)
case .loading:
progressLayer.update(size: progressFrame.size, lineWidth: 4.0, value: .indefinite, transition: transition)
progressLayer.update(size: progressFrame.size, lineWidth: indicatorLineUnseenWidth, value: .indefinite, transition: transition)
}
self.indicatorShapeSeenLayer.opacity = 0.0
self.indicatorShapeUnseenLayer.opacity = 0.0

View File

@ -1162,7 +1162,20 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
))
}, openPeer: { [weak self] peer, navigation, fromMessage, source in
self?.openPeer(peer: peer, navigation: navigation, fromMessage: fromMessage, fromReactionMessageId: source == .reaction ? fromMessage?.id : nil, expandAvatar: source == .groupParticipant)
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)
return
} else {
expandAvatar = true
}
}
var fromReactionMessageId: MessageId?
if case .reaction = source {
fromReactionMessageId = fromMessage?.id
}
self?.openPeer(peer: peer, navigation: navigation, fromMessage: fromMessage, fromReactionMessageId: fromReactionMessageId, expandAvatar: expandAvatar)
}, openPeerMention: { [weak self] name in
self?.openPeerMention(name)
}, openMessageContextMenu: { [weak self] message, selectAll, node, frame, anyRecognizer, location in
@ -16998,6 +17011,72 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})
}
private func openStories(peerId: EnginePeer.Id, avatarHeaderNode: ChatMessageAvatarHeaderNode) {
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
guard let self else {
return
}
var transitionIn: StoryContainerScreen.TransitionIn?
if let avatarHeaderNode {
transitionIn = StoryContainerScreen.TransitionIn(
sourceView: avatarHeaderNode.avatarNode.view,
sourceRect: avatarHeaderNode.avatarNode.view.bounds,
sourceCornerRadius: avatarHeaderNode.avatarNode.view.bounds.width * 0.5,
sourceIsAvatar: false
)
avatarHeaderNode.avatarNode.isHidden = true
}
let storyContainerScreen = StoryContainerScreen(
context: self.context,
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 {
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
}
)
}
)
self.push(storyContainerScreen)
})
}
private func openPeerMention(_ name: String, navigation: ChatControllerInteractionNavigateToPeer = .default, sourceMessageId: MessageId? = nil) {
let _ = self.presentVoiceMessageDiscardAlert(action: {
let disposable: MetaDisposable

View File

@ -101,7 +101,7 @@ func chatHistoryEntriesForView(
if let maybeJoinMessage = joinMessage {
if message.timestamp > maybeJoinMessage.timestamp, (!view.holeEarlier || count > 0) {
entries.append(.MessageEntry(maybeJoinMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false)))
entries.append(.MessageEntry(maybeJoinMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil)))
joinMessage = nil
}
}
@ -182,7 +182,7 @@ func chatHistoryEntriesForView(
} else {
selection = .none
}
groupBucket.append((message, isRead, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: false, isCentered: false), entry.location))
groupBucket.append((message, isRead, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: false, isCentered: false, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }), entry.location))
} else {
let selection: ChatHistoryMessageSelection
if let selectedMessages = selectedMessages {
@ -190,7 +190,7 @@ func chatHistoryEntriesForView(
} else {
selection = .none
}
entries.append(.MessageEntry(message, presentationData, isRead, entry.location, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: message.index == associatedData.currentlyPlayingMessageId, isCentered: false)))
entries.append(.MessageEntry(message, presentationData, isRead, entry.location, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: message.index == associatedData.currentlyPlayingMessageId, isCentered: false, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] })))
}
} else {
let selection: ChatHistoryMessageSelection
@ -199,7 +199,7 @@ func chatHistoryEntriesForView(
} else {
selection = .none
}
entries.append(.MessageEntry(message, presentationData, isRead, entry.location, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: message.index == associatedData.currentlyPlayingMessageId, isCentered: false)))
entries.append(.MessageEntry(message, presentationData, isRead, entry.location, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: message.index == associatedData.currentlyPlayingMessageId, isCentered: false, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] })))
}
}
@ -222,7 +222,7 @@ func chatHistoryEntriesForView(
}
if let maybeJoinMessage = joinMessage, !view.holeLater {
entries.append(.MessageEntry(maybeJoinMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false)))
entries.append(.MessageEntry(maybeJoinMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil)))
joinMessage = nil
}
@ -283,12 +283,12 @@ func chatHistoryEntriesForView(
if messages.count > 1, let groupInfo = messages[0].groupInfo {
var groupMessages: [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes, MessageHistoryEntryLocation?)] = []
for message in messages {
groupMessages.append((message, false, .none, ChatMessageEntryAttributes(rank: adminRank, isContact: false, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: false, isCentered: false), nil))
groupMessages.append((message, false, .none, ChatMessageEntryAttributes(rank: adminRank, isContact: false, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: false, isCentered: false, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }), nil))
}
entries.insert(.MessageGroupEntry(groupInfo, groupMessages, presentationData), at: 0)
} else {
if !hasTopicCreated {
entries.insert(.MessageEntry(messages[0], presentationData, false, nil, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: false, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[messages[0].id], isPlaying: false, isCentered: false)), at: 0)
entries.insert(.MessageEntry(messages[0], presentationData, false, nil, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: false, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[messages[0].id], isPlaying: false, isCentered: false, authorStoryStats: messages[0].author.flatMap { view.peerStoryStats[$0.id] })), at: 0)
}
}
@ -362,7 +362,7 @@ func chatHistoryEntriesForView(
if !dynamicAdMessages.isEmpty {
assert(entries.sorted() == entries)
for message in dynamicAdMessages {
entries.append(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false)))
entries.append(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil)))
}
entries.sort()
}
@ -396,7 +396,7 @@ func chatHistoryEntriesForView(
associatedStories: message.associatedStories
)
nextAdMessageId += 1
entries.append(.MessageEntry(updatedMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false)))
entries.append(.MessageEntry(updatedMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil)))
}
}
} else if includeSearchEntry {

View File

@ -18,14 +18,16 @@ public struct ChatMessageEntryAttributes: Equatable {
var updatingMedia: ChatUpdatingMessageMedia?
var isPlaying: Bool
var isCentered: Bool
var authorStoryStats: PeerStoryStats?
init(rank: CachedChannelAdminRank?, isContact: Bool, contentTypeHint: ChatMessageEntryContentType, updatingMedia: ChatUpdatingMessageMedia?, isPlaying: Bool, isCentered: Bool) {
init(rank: CachedChannelAdminRank?, isContact: Bool, contentTypeHint: ChatMessageEntryContentType, updatingMedia: ChatUpdatingMessageMedia?, isPlaying: Bool, isCentered: Bool, authorStoryStats: PeerStoryStats?) {
self.rank = rank
self.isContact = isContact
self.contentTypeHint = contentTypeHint
self.updatingMedia = updatingMedia
self.isPlaying = isPlaying
self.isCentered = isCentered
self.authorStoryStats = authorStoryStats
}
public init() {
@ -35,6 +37,7 @@ public struct ChatMessageEntryAttributes: Equatable {
self.updatingMedia = nil
self.isPlaying = false
self.isCentered = false
self.authorStoryStats = nil
}
}

View File

@ -374,8 +374,9 @@ final class ChatMessageAvatarHeader: ListViewItemHeader {
let presentationData: ChatPresentationData
let context: AccountContext
let controllerInteraction: ChatControllerInteraction
let storyStats: PeerStoryStats?
init(timestamp: Int32, peerId: PeerId, peer: Peer?, messageReference: MessageReference?, message: Message, presentationData: ChatPresentationData, context: AccountContext, controllerInteraction: ChatControllerInteraction) {
init(timestamp: Int32, peerId: PeerId, peer: Peer?, messageReference: MessageReference?, message: Message, presentationData: ChatPresentationData, context: AccountContext, controllerInteraction: ChatControllerInteraction, storyStats: PeerStoryStats?) {
self.peerId = peerId
self.peer = peer
self.messageReference = messageReference
@ -395,6 +396,7 @@ final class ChatMessageAvatarHeader: ListViewItemHeader {
self.context = context
self.controllerInteraction = controllerInteraction
self.id = ListViewItemNode.HeaderId(space: 1, id: Id(peerId: peerId, timestampId: dateHeaderTimestampId(timestamp: timestamp)))
self.storyStats = storyStats
}
let stickDirection: ListViewItemHeaderStickDirection = .top
@ -414,14 +416,15 @@ final class ChatMessageAvatarHeader: ListViewItemHeader {
}
func node(synchronousLoad: Bool) -> ListViewItemHeaderNode {
return ChatMessageAvatarHeaderNode(peerId: self.peerId, peer: self.peer, messageReference: self.messageReference, adMessageId: self.adMessageId, presentationData: self.presentationData, context: self.context, controllerInteraction: self.controllerInteraction, synchronousLoad: synchronousLoad)
return ChatMessageAvatarHeaderNode(peerId: self.peerId, peer: self.peer, messageReference: self.messageReference, adMessageId: self.adMessageId, presentationData: self.presentationData, context: self.context, controllerInteraction: self.controllerInteraction, storyStats: self.storyStats, synchronousLoad: synchronousLoad)
}
func updateNode(_ node: ListViewItemHeaderNode, previous: ListViewItemHeader?, next: ListViewItemHeader?) {
guard let node = node as? ChatMessageAvatarHeaderNode, let next = next as? ChatMessageAvatarHeader else {
guard let node = node as? ChatMessageAvatarHeaderNode else {
return
}
node.updatePresentationData(next.presentationData, context: next.context)
node.updatePresentationData(self.presentationData, context: self.context)
node.updateStoryStats(storyStats: self.storyStats, theme: self.presentationData.theme.theme, force: false)
}
}
@ -433,6 +436,7 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode {
private let context: AccountContext
private var presentationData: ChatPresentationData
private let controllerInteraction: ChatControllerInteraction
private var storyStats: PeerStoryStats?
private let peerId: PeerId
private let messageReference: MessageReference?
@ -440,7 +444,7 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode {
private let adMessageId: EngineMessage.Id?
private let containerNode: ContextControllerSourceNode
private let avatarNode: AvatarNode
let avatarNode: AvatarNode
private var avatarVideoNode: AvatarVideoNode?
private var cachedDataDisposable = MetaDisposable()
@ -459,7 +463,7 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode {
}
}
init(peerId: PeerId, peer: Peer?, messageReference: MessageReference?, adMessageId: EngineMessage.Id?, presentationData: ChatPresentationData, context: AccountContext, controllerInteraction: ChatControllerInteraction, synchronousLoad: Bool) {
init(peerId: PeerId, peer: Peer?, messageReference: MessageReference?, adMessageId: EngineMessage.Id?, presentationData: ChatPresentationData, context: AccountContext, controllerInteraction: ChatControllerInteraction, storyStats: PeerStoryStats?, synchronousLoad: Bool) {
self.peerId = peerId
self.peer = peer
self.messageReference = messageReference
@ -467,6 +471,7 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode {
self.presentationData = presentationData
self.context = context
self.controllerInteraction = controllerInteraction
self.storyStats = storyStats
self.containerNode = ContextControllerSourceNode()
@ -483,6 +488,10 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode {
self.setPeer(context: context, theme: presentationData.theme.theme, synchronousLoad: synchronousLoad, peer: peer, authorOfMessage: messageReference, emptyColor: .black)
}
if let storyStats {
self.updateStoryStats(storyStats: storyStats, theme: presentationData.theme.theme, force: true)
}
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self, let peer = strongSelf.peer else {
return
@ -596,6 +605,18 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode {
}
}
func updateStoryStats(storyStats: PeerStoryStats?, theme: PresentationTheme, force: Bool) {
if self.storyStats != storyStats || self.presentationData.theme.theme !== theme || force {
self.avatarNode.setStoryStats(storyStats: storyStats.flatMap { storyStats in
return AvatarNode.StoryStats(
totalCount: storyStats.totalCount,
unseenCount: storyStats.unseenCount,
hasUnseenCloseFriendsItems: false
)
}, theme: theme, transition: .immediate)
}
}
override func didLoad() {
super.didLoad()
@ -603,10 +624,11 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode {
}
func updatePresentationData(_ presentationData: ChatPresentationData, context: AccountContext) {
if self.presentationData !== presentationData {
self.presentationData = presentationData
self.setNeedsLayout()
}
}
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) {
self.containerNode.frame = CGRect(origin: CGPoint(x: leftInset + 3.0, y: 0.0), size: CGSize(width: 38.0, height: 38.0))
@ -663,7 +685,7 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode {
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
self.controllerInteraction.openPeer(EnginePeer(peer), .chat(textInputState: nil, subject: nil, peekData: nil), self.messageReference, .default)
} else {
self.controllerInteraction.openPeer(EnginePeer(peer), .info, self.messageReference, .groupParticipant)
self.controllerInteraction.openPeer(EnginePeer(peer), .info, self.messageReference, .groupParticipant(storyStats: self.storyStats, avatarHeaderNode: self))
}
}
}

View File

@ -406,7 +406,15 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible {
if hasAvatar {
if let effectiveAuthor = effectiveAuthor {
avatarHeader = ChatMessageAvatarHeader(timestamp: content.index.timestamp, peerId: effectiveAuthor.id, peer: effectiveAuthor, messageReference: MessageReference(message), message: message, presentationData: presentationData, context: context, controllerInteraction: controllerInteraction)
let storyStats: PeerStoryStats?
switch content {
case let .message(_, _, _, attributes, _):
storyStats = attributes.authorStoryStats
case let .group(messages):
storyStats = messages.first?.3.authorStoryStats
}
avatarHeader = ChatMessageAvatarHeader(timestamp: content.index.timestamp, peerId: effectiveAuthor.id, peer: effectiveAuthor, messageReference: MessageReference(message), message: message, presentationData: presentationData, context: context, controllerInteraction: controllerInteraction, storyStats: storyStats)
}
}
}