This commit is contained in:
Ali 2023-07-11 16:27:31 +04:00
parent 9672c77070
commit 6d7f74ecc3
21 changed files with 883 additions and 406 deletions

View File

@ -669,6 +669,8 @@ public final class AvatarNode: ASDisplayNode {
private var storyIndicator: ComponentView<Empty>?
public private(set) var storyPresentationParams: StoryPresentationParams?
private var loadingStatuses = Bag<Disposable>()
public struct StoryStats: Equatable {
public var totalCount: Int
public var unseenCount: Int
@ -742,6 +744,10 @@ public final class AvatarNode: ASDisplayNode {
self.addSubnode(self.contentNode)
}
deinit {
self.cancelLoading()
}
override public var frame: CGRect {
get {
return super.frame
@ -894,7 +900,8 @@ public final class AvatarNode: ASDisplayNode {
counters: AvatarStoryIndicatorComponent.Counters(
totalCount: storyStats.totalCount,
unseenCount: storyStats.unseenCount
)
),
displayProgress: !self.loadingStatuses.isEmpty
)),
environment: {},
containerSize: indicatorSize
@ -918,4 +925,43 @@ public final class AvatarNode: ASDisplayNode {
}
}
}
public func cancelLoading() {
for disposable in self.loadingStatuses.copyItems() {
disposable.dispose()
}
self.loadingStatuses.removeAll()
self.updateStoryIndicator(transition: .immediate)
}
public func pushLoadingStatus(signal: Signal<Never, NoError>) -> Disposable {
let disposable = MetaDisposable()
let index = self.loadingStatuses.add(disposable)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2, execute: { [weak self] in
self?.updateStoryIndicator(transition: .immediate)
})
disposable.set(signal.start(completed: { [weak self] in
Queue.mainQueue().async {
guard let self else {
return
}
self.loadingStatuses.remove(index)
if self.loadingStatuses.isEmpty {
self.updateStoryIndicator(transition: .immediate)
}
}
}))
return ActionDisposable { [weak self] in
guard let self else {
return
}
self.loadingStatuses.remove(index)
if self.loadingStatuses.isEmpty {
self.updateStoryIndicator(transition: .immediate)
}
}
}
}

View File

@ -1337,87 +1337,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let self else {
return
}
let isHidden: Bool
let focusedPeerId: EnginePeer.Id?
let singlePeer: Bool
switch subject {
case let .peer(peerId):
isHidden = self.location == .chatList(groupId: .archive)
focusedPeerId = peerId
singlePeer = true
case .archive:
isHidden = true
focusedPeerId = nil
singlePeer = false
guard let itemNode = itemNode as? ChatListItemNode else {
return
}
let storyContent = StoryContentContextImpl(context: self.context, isHidden: isHidden, focusedPeerId: focusedPeerId, singlePeer: singlePeer)
let _ = (storyContent.state
|> filter { $0.slice != nil }
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self, weak itemNode] state in
guard let self else {
return
}
var transitionIn: StoryContainerScreen.TransitionIn?
if let itemNode = itemNode as? ChatListItemNode {
transitionIn = StoryContainerScreen.TransitionIn(
sourceView: itemNode.avatarNode.view,
sourceRect: itemNode.avatarNode.view.bounds,
sourceCornerRadius: itemNode.avatarNode.view.bounds.height * 0.5,
sourceIsAvatar: true
)
itemNode.avatarNode.isHidden = true
}
let storyContainerScreen = StoryContainerScreen(
context: self.context,
content: storyContent,
transitionIn: transitionIn,
transitionOut: { _, _ in
if let itemNode = itemNode as? ChatListItemNode {
let transitionView = itemNode.avatarNode.view
let destinationView = itemNode.view
let rect = transitionView.convert(transitionView.bounds, to: destinationView)
return StoryContainerScreen.TransitionOut(
destinationView: destinationView,
transitionView: StoryContainerScreen.TransitionView(
makeView: { [weak transitionView] in
let parentView = UIView()
if let copyView = transitionView?.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: CGSize(width: state.destinationSize.width, height: state.destinationSize.height), amount: state.progress)
let scaleSize = state.sourceSize.interpolate(to: CGSize(width: state.destinationSize.width - 7.0, height: state.destinationSize.height - 7.0), amount: state.progress)
transition.setPosition(view: view, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
transition.setScale(view: view, scale: scaleSize.width / state.destinationSize.width)
},
insertCloneTransitionView: nil
),
destinationRect: rect,
destinationCornerRadius: rect.height * 0.5,
destinationIsAvatar: true,
completed: { [weak itemNode] in
guard let itemNode else {
return
}
itemNode.avatarNode.isHidden = false
}
)
}
return nil
}
)
self.push(storyContainerScreen)
})
switch subject {
case .archive:
StoryContainerScreen.openArchivedStories(context: self.context, parentController: self, avatarNode: itemNode.avatarNode)
case let .peer(peerId):
StoryContainerScreen.openPeerStories(context: self.context, peerId: peerId, parentController: self, avatarNode: itemNode.avatarNode)
}
}
self.chatListDisplayNode.peerContextAction = { [weak self] peer, source, node, gesture, location in
@ -3608,6 +3537,88 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.shouldFixStorySubscriptionOrder = true
}
}
if peerId != self.context.account.peerId {
if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View {
if navigationBarView.storiesUnlocked {
if let componentView = self.chatListHeaderView(), let storyPeerListView = componentView.storyPeerListView() {
let _ = storyPeerListView
StoryContainerScreen.openPeerStoriesCustom(
context: self.context,
peerId: peerId,
isHidden: self.location == .chatList(groupId: .archive),
singlePeer: false,
parentController: self,
transitionIn: { [weak self] in
guard let self else {
return nil
}
var transitionIn: StoryContainerScreen.TransitionIn?
if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View {
if navigationBarView.storiesUnlocked {
if let componentView = self.chatListHeaderView() {
if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) {
transitionIn = StoryContainerScreen.TransitionIn(
sourceView: transitionView,
sourceRect: transitionView.bounds,
sourceCornerRadius: transitionView.bounds.height * 0.5,
sourceIsAvatar: true
)
}
}
}
}
return transitionIn
},
transitionOut: { [weak self] peerId in
guard let self else {
return nil
}
if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View {
if navigationBarView.storiesUnlocked {
if let componentView = self.chatListHeaderView() {
if let (transitionView, transitionContentView) = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) {
return StoryContainerScreen.TransitionOut(
destinationView: transitionView,
transitionView: transitionContentView,
destinationRect: transitionView.bounds,
destinationCornerRadius: transitionView.bounds.height * 0.5,
destinationIsAvatar: true,
completed: {}
)
}
}
}
}
return nil
},
setFocusedItem: { [weak self] focusedItem in
guard let self else {
return
}
if let componentView = self.chatListHeaderView() {
componentView.storyPeerListView()?.setPreviewedItem(signal: focusedItem)
}
},
setProgress: { [weak self] signal in
guard let self else {
return
}
if let componentView = self.chatListHeaderView() {
componentView.storyPeerListView()?.setLoadingItem(peerId: peerId, signal: signal)
}
}
)
return
}
}
}
}
let storyContent = StoryContentContextImpl(context: self.context, isHidden: self.location == .chatList(groupId: .archive), focusedPeerId: peerId, singlePeer: false, fixedOrder: self.fixedStorySubscriptionOrder)
let _ = (storyContent.state
|> take(1)

View File

@ -26,7 +26,6 @@ import TextNodeWithEntities
import ComponentFlow
import EmojiStatusComponent
import AvatarVideoNode
import AvatarStoryIndicatorComponent
public enum ChatListItemContent {
public struct ThreadInfo: Equatable {
@ -941,7 +940,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
var avatarIconView: ComponentHostView<Empty>?
var avatarIconComponent: EmojiStatusComponent?
var avatarVideoNode: AvatarVideoNode?
var avatarStoryIndicator: ComponentView<Empty>?
var avatarTapRecognizer: UITapGestureRecognizer?
private var inlineNavigationMarkLayer: SimpleLayer?
@ -1310,6 +1309,15 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
}
item.interaction.activateChatPreview(item, threadId, strongSelf.contextContainer, gesture, nil)
}
self.onDidLoad { [weak self] _ in
guard let self else {
return
}
let avatarTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.avatarStoryTapGesture(_:)))
self.avatarTapRecognizer = avatarTapRecognizer
self.avatarNode.view.addGestureRecognizer(avatarTapRecognizer)
}
}
deinit {
@ -1327,28 +1335,48 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let previousItem = self.item
self.item = item
var storyState: ChatListItemContent.StoryState?
if case let .peer(peerData) = item.content {
storyState = peerData.storyState
} else if case let .groupReference(groupReference) = item.content {
storyState = groupReference.storyState
}
var peer: EnginePeer?
var displayAsMessage = false
var enablePreview = true
switch item.content {
case let .peer(peerData):
displayAsMessage = peerData.displayAsMessage
if displayAsMessage, case let .user(author) = peerData.messages.last?.author {
peer = .user(author)
} else {
peer = peerData.peer.chatMainPeer
}
if peerData.peer.peerId.namespace == Namespaces.Peer.SecretChat {
enablePreview = false
}
case let .groupReference(groupReferenceData):
if let previousItem = previousItem, case let .groupReference(previousGroupReferenceData) = previousItem.content, groupReferenceData.hiddenByDefault != previousGroupReferenceData.hiddenByDefault {
UIView.transition(with: self.avatarNode.view, duration: 0.3, options: [.transitionCrossDissolve], animations: {
}, completion: nil)
}
self.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: .archivedChatsIcon(hiddenByDefault: groupReferenceData.hiddenByDefault), emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads)
case let .peer(peerData):
displayAsMessage = peerData.displayAsMessage
if displayAsMessage, case let .user(author) = peerData.messages.last?.author {
peer = .user(author)
} else {
peer = peerData.peer.chatMainPeer
}
if peerData.peer.peerId.namespace == Namespaces.Peer.SecretChat {
enablePreview = false
}
case let .groupReference(groupReferenceData):
if let previousItem = previousItem, case let .groupReference(previousGroupReferenceData) = previousItem.content, groupReferenceData.hiddenByDefault != previousGroupReferenceData.hiddenByDefault {
UIView.transition(with: self.avatarNode.view, duration: 0.3, options: [.transitionCrossDissolve], animations: {
}, completion: nil)
}
self.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: .archivedChatsIcon(hiddenByDefault: groupReferenceData.hiddenByDefault), emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads)
}
self.avatarNode.setStoryStats(storyStats: storyState.flatMap { storyState in
return AvatarNode.StoryStats(
totalCount: storyState.stats.totalCount,
unseenCount: storyState.stats.unseenCount,
hasUnseenCloseFriendsItems: storyState.hasUnseenCloseFriends
)
}, presentationParams: AvatarNode.StoryPresentationParams(
colors: AvatarNode.Colors(theme: item.presentationData.theme),
lineWidth: 2.33,
inactiveLineWidth: 1.33
), transition: .immediate)
self.avatarTapRecognizer?.isEnabled = storyState != nil
if let peer = peer {
var overrideImage: AvatarNodeImageOverride?
if peer.id.isReplies {
@ -2792,13 +2820,6 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let contentRect = rawContentRect.offsetBy(dx: editingOffset + leftInset + revealOffset, dy: 0.0)
var storyState: ChatListItemContent.StoryState?
if case let .peer(peerData) = item.content {
storyState = peerData.storyState
} else if case let .groupReference(groupReference) = item.content {
storyState = groupReference.storyState
}
let avatarFrame = CGRect(origin: CGPoint(x: leftInset - avatarLeftInset + editingOffset + 10.0 + revealOffset, y: floor((itemHeight - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter))
var avatarScaleOffset: CGFloat = 0.0
var avatarScale: CGFloat = 1.0
@ -2810,11 +2831,6 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
avatarScaleOffset = targetAvatarScaleOffset * inlineNavigationLocation.progress
}
let storyIndicatorScale = avatarScale
if storyState != nil {
avatarScale *= (avatarFrame.width - 4.0 * 2.0) / avatarFrame.width
}
transition.updateFrame(node: strongSelf.avatarContainerNode, frame: avatarFrame)
transition.updatePosition(node: strongSelf.avatarNode, position: avatarFrame.offsetBy(dx: -avatarFrame.minX, dy: -avatarFrame.minY).center.offsetBy(dx: avatarScaleOffset, dy: 0.0))
transition.updateBounds(node: strongSelf.avatarNode, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
@ -2822,55 +2838,6 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.avatarNode.updateSize(size: avatarFrame.size)
strongSelf.updateVideoVisibility()
if let storyState {
var indicatorTransition = Transition(transition)
let avatarStoryIndicator: ComponentView<Empty>
if let current = strongSelf.avatarStoryIndicator {
avatarStoryIndicator = current
} else {
indicatorTransition = .immediate
avatarStoryIndicator = ComponentView()
strongSelf.avatarStoryIndicator = avatarStoryIndicator
}
var indicatorFrame = CGRect(origin: CGPoint(x: avatarFrame.minX + 4.0, y: avatarFrame.minY + 4.0), size: CGSize(width: avatarFrame.width - 4.0 - 4.0, height: avatarFrame.height - 4.0 - 4.0))
indicatorFrame.origin.x -= (avatarFrame.width - avatarFrame.width * storyIndicatorScale) * 0.5
let _ = avatarStoryIndicator.update(
transition: indicatorTransition,
component: AnyComponent(AvatarStoryIndicatorComponent(
hasUnseen: storyState.stats.unseenCount != 0,
hasUnseenCloseFriendsItems: storyState.hasUnseenCloseFriends,
colors: AvatarStoryIndicatorComponent.Colors(theme: item.presentationData.theme),
activeLineWidth: 2.33,
inactiveLineWidth: 1.33,
counters: AvatarStoryIndicatorComponent.Counters(
totalCount: storyState.stats.totalCount,
unseenCount: storyState.stats.unseenCount
)
)),
environment: {},
containerSize: indicatorFrame.size
)
if let avatarStoryIndicatorView = avatarStoryIndicator.view {
if avatarStoryIndicatorView.superview == nil {
avatarStoryIndicatorView.isUserInteractionEnabled = true
avatarStoryIndicatorView.addGestureRecognizer(UITapGestureRecognizer(target: strongSelf, action: #selector(strongSelf.avatarStoryTapGesture(_:))))
strongSelf.contextContainer.view.insertSubview(avatarStoryIndicatorView, belowSubview: strongSelf.avatarContainerNode.view)
}
indicatorTransition.setPosition(view: avatarStoryIndicatorView, position: indicatorFrame.center)
indicatorTransition.setBounds(view: avatarStoryIndicatorView, bounds: CGRect(origin: CGPoint(), size: indicatorFrame.size))
indicatorTransition.setScale(view: avatarStoryIndicatorView, scale: storyIndicatorScale)
}
} else {
if let avatarStoryIndicator = strongSelf.avatarStoryIndicator {
strongSelf.avatarStoryIndicator = nil
avatarStoryIndicator.view?.removeFromSuperview()
}
}
var itemPeerId: EnginePeer.Id?
if case let .chatList(index) = item.index {
itemPeerId = index.messageIndex.id.peerId
@ -3900,8 +3867,10 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
if let _ = item.interaction.inlineNavigationLocation {
} else {
if let avatarStoryIndicatorView = self.avatarStoryIndicator?.view, let result = avatarStoryIndicatorView.hitTest(self.view.convert(point, to: avatarStoryIndicatorView), with: event) {
return result
if self.avatarNode.storyStats != nil {
if let result = self.avatarNode.view.hitTest(self.view.convert(point, to: self.avatarNode.view), with: event) {
return result
}
}
}
@ -3919,7 +3888,6 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
case .groupReference:
item.interaction.openStories(.archive, self)
}
}
}
}

View File

@ -2403,6 +2403,7 @@ public final class ChatListNode: ListView {
strongSelf.enqueueHistoryPreloadUpdate()
}
var refreshStoryPeerIds: [PeerId] = []
var isHiddenItemVisible = false
if let range = range.visibleRange {
let entryCount = chatListView.filteredEntries.count
@ -2418,6 +2419,11 @@ public final class ChatListNode: ListView {
if let threadInfo, threadInfo.isHidden {
isHiddenItemVisible = true
}
if let peer = peerEntry.peer.chatMainPeer, !peerEntry.isContact, case let .user(user) = peer {
refreshStoryPeerIds.append(user.id)
}
break
case .GroupReferenceEntry:
isHiddenItemVisible = true
@ -2433,6 +2439,9 @@ public final class ChatListNode: ListView {
return state
}
}
if !refreshStoryPeerIds.isEmpty {
strongSelf.context.account.viewTracker.refreshStoryStatsForPeerIds(peerIds: refreshStoryPeerIds)
}
}
}

View File

@ -512,52 +512,10 @@ public class ContactsController: ViewController {
guard let self else {
return
}
let storyContent = StoryContentContextImpl(context: self.context, isHidden: true, focusedPeerId: peer.id, singlePeer: true)
let _ = (storyContent.state
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self, weak sourceNode] storyContentState in
guard let self else {
return
}
var transitionIn: StoryContainerScreen.TransitionIn?
if let itemNode = sourceNode as? ContactsPeerItemNode {
transitionIn = StoryContainerScreen.TransitionIn(
sourceView: itemNode.avatarNode.view,
sourceRect: itemNode.avatarNode.view.bounds,
sourceCornerRadius: itemNode.avatarNode.view.bounds.height * 0.5,
sourceIsAvatar: true
)
itemNode.avatarNode.isHidden = true
}
let storyContainerScreen = StoryContainerScreen(
context: self.context,
content: storyContent,
transitionIn: transitionIn,
transitionOut: { _, _ in
if let itemNode = sourceNode as? ContactsPeerItemNode {
let rect = itemNode.avatarNode.view.convert(itemNode.avatarNode.view.bounds, to: itemNode.view)
return StoryContainerScreen.TransitionOut(
destinationView: itemNode.view,
transitionView: nil,
destinationRect: rect,
destinationCornerRadius: rect.height * 0.5,
destinationIsAvatar: true,
completed: { [weak itemNode] in
guard let itemNode else {
return
}
itemNode.avatarNode.isHidden = false
}
)
}
return nil
}
)
self.push(storyContainerScreen)
})
if let itemNode = sourceNode as? ContactsPeerItemNode {
StoryContainerScreen.openPeerStories(context: self.context, peerId: peer.id, parentController: self, avatarNode: itemNode.avatarNode)
}
}
}

View File

@ -274,14 +274,17 @@ func fetchPeerStoryStats(postbox: PostboxImpl, peerId: PeerId) -> PeerStoryStats
if topItems.id == 0 {
return nil
}
guard let state = postbox.storyPeerStatesTable.get(key: .peer(peerId)) else {
return nil
var maxSeenId: Int32 = 0
if let state = postbox.storyPeerStatesTable.get(key: .peer(peerId)) {
maxSeenId = state.maxSeenId
}
if topItems.isExact {
let stats = postbox.storyItemsTable.getStats(peerId: peerId, maxSeenId: state.maxSeenId)
let stats = postbox.storyItemsTable.getStats(peerId: peerId, maxSeenId: maxSeenId)
return PeerStoryStats(totalCount: stats.total, unseenCount: stats.unseen)
} else {
return PeerStoryStats(totalCount: 1, unseenCount: topItems.id > state.maxSeenId ? 1 : 0)
return PeerStoryStats(totalCount: 1, unseenCount: topItems.id > maxSeenId ? 1 : 0)
}
}

View File

@ -296,6 +296,9 @@ public final class AccountViewTracker {
private var refreshStoriesForMessageIdsAndTimestamps: [MessageId: Int32] = [:]
private var nextUpdatedUnsupportedMediaDisposableId: Int32 = 0
private var updatedUnsupportedMediaDisposables = DisposableDict<Int32>()
private var refreshStoriesForPeerIdsAndTimestamps: [PeerId: Int32] = [:]
private var refreshStoriesForPeerIdsDebounceDisposable: Disposable?
private var pendingRefreshStoriesForPeerIds: [PeerId] = []
private var updatedSeenPersonalMessageIds = Set<MessageId>()
private var updatedReactionsSeenForMessageIds = Set<MessageId>()
@ -1262,6 +1265,89 @@ public final class AccountViewTracker {
}
}
public func refreshStoryStatsForPeerIds(peerIds: [PeerId]) {
self.queue.async {
self.pendingRefreshStoriesForPeerIds.append(contentsOf: peerIds)
if self.refreshStoriesForPeerIdsDebounceDisposable == nil {
self.refreshStoriesForPeerIdsDebounceDisposable = (Signal<Never, NoError>.complete() |> delay(0.15, queue: self.queue)).start(completed: {
self.refreshStoriesForPeerIdsDebounceDisposable = nil
let pendingPeerIds = self.pendingRefreshStoriesForPeerIds
self.pendingRefreshStoriesForPeerIds.removeAll()
self.internalRefreshStoryStatsForPeerIds(peerIds: pendingPeerIds)
})
}
}
}
private func internalRefreshStoryStatsForPeerIds(peerIds: [PeerId]) {
self.queue.async {
var addedPeerIds: [PeerId] = []
let timestamp = Int32(CFAbsoluteTimeGetCurrent())
for peerId in peerIds {
let messageTimestamp = self.refreshStoriesForPeerIdsAndTimestamps[peerId]
var refresh = false
if let messageTimestamp = messageTimestamp {
refresh = messageTimestamp < timestamp - 60
} else {
refresh = true
}
if refresh {
self.refreshStoriesForPeerIdsAndTimestamps[peerId] = timestamp
addedPeerIds.append(peerId)
}
}
if !addedPeerIds.isEmpty {
let disposableId = self.nextUpdatedUnsupportedMediaDisposableId
self.nextUpdatedUnsupportedMediaDisposableId += 1
if let account = self.account {
let signal = account.postbox.transaction { transaction -> [Api.InputUser] in
return addedPeerIds.compactMap { transaction.getPeer($0).flatMap(apiInputUser) }
}
|> mapToSignal { inputUsers -> Signal<Never, NoError> in
guard !inputUsers.isEmpty else {
return .complete()
}
var requests: [Signal<Never, NoError>] = []
let batchCount = 50
var startIndex = 0
while startIndex < inputUsers.count {
var slice: [Api.InputUser] = []
for i in startIndex ..< min(startIndex + batchCount, inputUsers.count) {
slice.append(inputUsers[i])
}
startIndex += batchCount
requests.append(account.network.request(Api.functions.users.getUsers(id: slice))
|> `catch` { _ -> Signal<[Api.User], NoError> in
return .single([])
}
|> mapToSignal { result -> Signal<Never, NoError> in
return account.postbox.transaction { transaction in
updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(users: result))
}
|> ignoreValues
})
}
return combineLatest(requests)
|> ignoreValues
}
|> afterDisposed { [weak self] in
self?.queue.async {
self?.updatedUnsupportedMediaDisposables.set(nil, forKey: disposableId)
}
}
self.updatedUnsupportedMediaDisposables.set(signal.start(), forKey: disposableId)
}
}
}
}
public func updateMarkAllMentionsSeen(peerId: PeerId, threadId: Int64?) {
self.queue.async {
guard let account = self.account else {

View File

@ -49,6 +49,9 @@ func updatePeers(transaction: Transaction, accountPeerId: PeerId, peers: Accumul
if let storiesMaxId = storiesMaxId {
transaction.setStoryItemsInexactMaxId(peerId: user.peerId, id: storiesMaxId)
}
/*#if DEBUG
transaction.setStoryItemsInexactMaxId(peerId: user.peerId, id: 10)
#endif*/
case .userEmpty:
break
}

View File

@ -13,6 +13,7 @@ swift_library(
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/TelegramPresentationData",
"//submodules/Components/HierarchyTrackingLayer",
],
visibility = [
"//visibility:public",

View File

@ -2,6 +2,7 @@ import Foundation
import UIKit
import Display
import ComponentFlow
import HierarchyTrackingLayer
import TelegramPresentationData
public final class AvatarStoryIndicatorComponent: Component {
@ -43,6 +44,7 @@ public final class AvatarStoryIndicatorComponent: Component {
public let activeLineWidth: CGFloat
public let inactiveLineWidth: CGFloat
public let counters: Counters?
public let displayProgress: Bool
public init(
hasUnseen: Bool,
@ -50,7 +52,8 @@ public final class AvatarStoryIndicatorComponent: Component {
colors: Colors,
activeLineWidth: CGFloat,
inactiveLineWidth: CGFloat,
counters: Counters?
counters: Counters?,
displayProgress: Bool = false
) {
self.hasUnseen = hasUnseen
self.hasUnseenCloseFriendsItems = hasUnseenCloseFriendsItems
@ -58,6 +61,7 @@ public final class AvatarStoryIndicatorComponent: Component {
self.activeLineWidth = activeLineWidth
self.inactiveLineWidth = inactiveLineWidth
self.counters = counters
self.displayProgress = displayProgress
}
public static func ==(lhs: AvatarStoryIndicatorComponent, rhs: AvatarStoryIndicatorComponent) -> Bool {
@ -79,11 +83,167 @@ public final class AvatarStoryIndicatorComponent: Component {
if lhs.counters != rhs.counters {
return false
}
if lhs.displayProgress != rhs.displayProgress {
return false
}
return true
}
private final class ProgressLayer: HierarchyTrackingLayer {
enum Value: Equatable {
case indefinite
case progress(Float)
}
private struct Params: Equatable {
var size: CGSize
var lineWidth: CGFloat
var value: Value
}
private var currentParams: Params?
private let uploadProgressLayer = SimpleShapeLayer()
private let indefiniteDashLayer = SimpleShapeLayer()
private let indefiniteReplicatorLayer = CAReplicatorLayer()
override init() {
super.init()
self.uploadProgressLayer.fillColor = nil
self.uploadProgressLayer.strokeColor = UIColor.white.cgColor
self.uploadProgressLayer.lineCap = .round
self.indefiniteDashLayer.fillColor = nil
self.indefiniteDashLayer.strokeColor = UIColor.white.cgColor
self.indefiniteDashLayer.lineCap = .round
self.indefiniteDashLayer.lineJoin = .round
self.indefiniteDashLayer.strokeEnd = 0.0333
let count = 1.0 / self.indefiniteDashLayer.strokeEnd
let angle = (2.0 * Double.pi) / Double(count)
self.indefiniteReplicatorLayer.addSublayer(self.indefiniteDashLayer)
self.indefiniteReplicatorLayer.instanceCount = Int(count)
self.indefiniteReplicatorLayer.instanceTransform = CATransform3DMakeRotation(CGFloat(angle), 0.0, 0.0, 1.0)
self.indefiniteReplicatorLayer.transform = CATransform3DMakeRotation(-.pi / 2.0, 0.0, 0.0, 1.0)
self.indefiniteReplicatorLayer.instanceDelay = 0.025
self.didEnterHierarchy = { [weak self] in
guard let self else {
return
}
self.updateAnimations(transition: .immediate)
}
}
override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func reset() {
self.currentParams = nil
self.indefiniteDashLayer.path = nil
self.uploadProgressLayer.path = nil
}
func updateAnimations(transition: Transition) {
guard let params = self.currentParams else {
return
}
switch params.value {
case let .progress(progress):
if self.indefiniteReplicatorLayer.superlayer != nil {
self.indefiniteReplicatorLayer.removeFromSuperlayer()
}
if self.uploadProgressLayer.superlayer == nil {
self.addSublayer(self.uploadProgressLayer)
}
transition.setShapeLayerStrokeEnd(layer: self.uploadProgressLayer, strokeEnd: CGFloat(progress))
if self.uploadProgressLayer.animation(forKey: "rotation") == nil {
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationAnimation.duration = 2.0
rotationAnimation.fromValue = NSNumber(value: Float(0.0))
rotationAnimation.toValue = NSNumber(value: Float(Double.pi * 2.0))
rotationAnimation.repeatCount = Float.infinity
rotationAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
self.uploadProgressLayer.add(rotationAnimation, forKey: "rotation")
}
case .indefinite:
if self.uploadProgressLayer.superlayer == nil {
self.uploadProgressLayer.removeFromSuperlayer()
}
if self.indefiniteReplicatorLayer.superlayer == nil {
self.addSublayer(self.indefiniteReplicatorLayer)
}
if self.indefiniteReplicatorLayer.animation(forKey: "rotation") == nil {
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationAnimation.duration = 4.0
rotationAnimation.fromValue = NSNumber(value: -.pi / 2.0)
rotationAnimation.toValue = NSNumber(value: -.pi / 2.0 + Double.pi * 2.0)
rotationAnimation.repeatCount = Float.infinity
rotationAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
self.indefiniteReplicatorLayer.add(rotationAnimation, forKey: "rotation")
}
if self.indefiniteDashLayer.animation(forKey: "dash") == nil {
let dashAnimation = CAKeyframeAnimation(keyPath: "strokeStart")
dashAnimation.keyTimes = [0.0, 0.45, 0.55, 1.0]
dashAnimation.values = [
self.indefiniteDashLayer.strokeStart,
self.indefiniteDashLayer.strokeEnd,
self.indefiniteDashLayer.strokeEnd,
self.indefiniteDashLayer.strokeStart,
]
dashAnimation.timingFunction = CAMediaTimingFunction(name: .linear)
dashAnimation.duration = 2.5
dashAnimation.repeatCount = .infinity
self.indefiniteDashLayer.add(dashAnimation, forKey: "dash")
}
}
}
func update(size: CGSize, radius: CGFloat, lineWidth: CGFloat, value: Value, transition: Transition) {
let params = Params(
size: size,
lineWidth: lineWidth,
value: value
)
if self.currentParams == params {
return
}
self.currentParams = params
self.indefiniteDashLayer.lineWidth = lineWidth
self.uploadProgressLayer.lineWidth = lineWidth
let bounds = CGRect(origin: .zero, size: size)
if self.uploadProgressLayer.path == nil {
let path = CGMutablePath()
path.addEllipse(in: CGRect(origin: CGPoint(x: (size.width - radius * 2.0) * 0.5, y: (size.height - radius * 2.0) * 0.5), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
self.uploadProgressLayer.path = path
self.uploadProgressLayer.frame = bounds
}
if self.indefiniteDashLayer.path == nil {
let path = CGMutablePath()
path.addEllipse(in: CGRect(origin: CGPoint(x: (size.width - radius * 2.0) * 0.5, y: (size.height - radius * 2.0) * 0.5), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
self.indefiniteDashLayer.path = path
self.indefiniteReplicatorLayer.frame = bounds
self.indefiniteDashLayer.frame = bounds
}
self.updateAnimations(transition: transition)
}
}
public final class View: UIView {
private let indicatorView: UIImageView
private var progressLayer: ProgressLayer?
private var colorLayer: SimpleGradientLayer?
private var component: AvatarStoryIndicatorComponent?
private weak var state: EmptyComponentState?
@ -110,25 +270,26 @@ public final class AvatarStoryIndicatorComponent: Component {
diameter = availableSize.width + maxOuterInset * 2.0
let imageDiameter = availableSize.width + ceilToScreenPixels(maxOuterInset) * 2.0
let activeColors: [CGColor]
let inactiveColors: [CGColor]
if component.hasUnseenCloseFriendsItems {
activeColors = component.colors.unseenCloseFriendsColors.map(\.cgColor)
} else {
activeColors = component.colors.unseenColors.map(\.cgColor)
}
inactiveColors = component.colors.seenColors.map(\.cgColor)
let radius = (diameter - component.activeLineWidth) * 0.5
self.indicatorView.image = generateImage(CGSize(width: imageDiameter, height: imageDiameter), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
let activeColors: [CGColor]
let inactiveColors: [CGColor]
if component.hasUnseenCloseFriendsItems {
activeColors = component.colors.unseenCloseFriendsColors.map(\.cgColor)
} else {
activeColors = component.colors.unseenColors.map(\.cgColor)
}
inactiveColors = component.colors.seenColors.map(\.cgColor)
var locations: [CGFloat] = [0.0, 1.0]
if let counters = component.counters, counters.totalCount > 1 {
let center = CGPoint(x: size.width * 0.5, y: size.height * 0.5)
let radius = (diameter - component.activeLineWidth) * 0.5
let spacing: CGFloat = 2.0
let angularSpacing: CGFloat = spacing / radius
let circleLength = CGFloat.pi * 2.0 * radius
@ -197,7 +358,61 @@ public final class AvatarStoryIndicatorComponent: Component {
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
}
})
transition.setFrame(view: self.indicatorView, frame: CGRect(origin: CGPoint(x: (availableSize.width - imageDiameter) * 0.5, y: (availableSize.height - imageDiameter) * 0.5), size: CGSize(width: imageDiameter, height: imageDiameter)))
let indicatorFrame = CGRect(origin: CGPoint(x: (availableSize.width - imageDiameter) * 0.5, y: (availableSize.height - imageDiameter) * 0.5), size: CGSize(width: imageDiameter, height: imageDiameter))
transition.setFrame(view: self.indicatorView, frame: indicatorFrame)
let progressTransition = Transition(animation: .curve(duration: 0.3, curve: .easeInOut))
if component.displayProgress {
let colorLayer: SimpleGradientLayer
if let current = self.colorLayer {
colorLayer = current
} else {
colorLayer = SimpleGradientLayer()
self.colorLayer = colorLayer
self.layer.addSublayer(colorLayer)
colorLayer.opacity = 0.0
}
progressTransition.setAlpha(view: self.indicatorView, alpha: 0.0)
progressTransition.setAlpha(layer: colorLayer, alpha: 1.0)
let colors: [CGColor] = activeColors
/*if component.hasUnseen {
colors = activeColors
} else {
colors = inactiveColors
}*/
let lineWidth: CGFloat = component.hasUnseen ? component.activeLineWidth : component.inactiveLineWidth
colorLayer.colors = colors
colorLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
colorLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
let progressLayer: ProgressLayer
if let current = self.progressLayer {
progressLayer = current
} else {
progressLayer = ProgressLayer()
self.progressLayer = progressLayer
colorLayer.mask = progressLayer
}
colorLayer.frame = indicatorFrame
progressLayer.frame = CGRect(origin: CGPoint(), size: indicatorFrame.size)
progressLayer.update(size: indicatorFrame.size, radius: radius, lineWidth: lineWidth, value: .indefinite, transition: .immediate)
} else {
progressTransition.setAlpha(view: self.indicatorView, alpha: 1.0)
self.progressLayer = nil
if let colorLayer = self.colorLayer {
self.colorLayer = nil
progressTransition.setAlpha(layer: colorLayer, alpha: 0.0, completion: { [weak colorLayer] _ in
colorLayer?.removeFromSuperlayer()
})
}
}
return availableSize
}

View File

@ -57,7 +57,7 @@ public final class PeerListItemComponent: Component {
let selectionState: SelectionState
let hasNext: Bool
let action: (EnginePeer) -> Void
let openStories: ((EnginePeer, UIView) -> Void)?
let openStories: ((EnginePeer, AvatarNode) -> Void)?
public init(
context: AccountContext,
@ -74,7 +74,7 @@ public final class PeerListItemComponent: Component {
selectionState: SelectionState,
hasNext: Bool,
action: @escaping (EnginePeer) -> Void,
openStories: ((EnginePeer, UIView) -> Void)? = nil
openStories: ((EnginePeer, AvatarNode) -> Void)? = nil
) {
self.context = context
self.theme = theme
@ -211,7 +211,7 @@ public final class PeerListItemComponent: Component {
guard let component = self.component, let peer = component.peer else {
return
}
component.openStories?(peer, self.avatarNode.view)
component.openStories?(peer, self.avatarNode)
}
func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {

View File

@ -76,6 +76,7 @@ swift_library(
"//submodules/OpenInExternalAppUI",
"//submodules/MediaPasteboardUI",
"//submodules/WebPBinding",
"//submodules/Utils/RangeSet",
],
visibility = [
"//visibility:public",

View File

@ -0,0 +1,205 @@
import Foundation
import UIKit
import Display
import AccountContext
import SwiftSignalKit
import TelegramCore
import Postbox
import AvatarNode
public extension StoryContainerScreen {
static func openArchivedStories(context: AccountContext, parentController: ViewController, avatarNode: AvatarNode) {
let storyContent = StoryContentContextImpl(context: context, isHidden: true, focusedPeerId: nil, singlePeer: false)
let signal = storyContent.state
|> take(1)
|> mapToSignal { state -> Signal<Void, NoError> in
if let slice = state.slice {
return waitUntilStoryMediaPreloaded(context: context, peerId: slice.peer.id, storyItem: slice.item.storyItem)
|> timeout(2.0, queue: .mainQueue(), alternate: .complete())
|> map { _ -> Void in
}
|> then(.single(Void()))
} else {
return .single(Void())
}
}
|> deliverOnMainQueue
|> map { [weak parentController, weak avatarNode] _ -> Void in
var transitionIn: StoryContainerScreen.TransitionIn?
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(
context: context,
content: storyContent,
transitionIn: transitionIn,
transitionOut: { peerId, _ in
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
}
}
)
parentController?.push(storyContainerScreen)
}
|> ignoreValues
let _ = avatarNode.pushLoadingStatus(signal: signal)
}
static func openPeerStories(context: AccountContext, peerId: EnginePeer.Id, parentController: ViewController, avatarNode: AvatarNode) {
return openPeerStoriesCustom(
context: context,
peerId: peerId,
isHidden: false,
singlePeer: true,
parentController: parentController,
transitionIn: { [weak avatarNode] in
if let avatarNode {
let transitionIn = StoryContainerScreen.TransitionIn(
sourceView: avatarNode.view,
sourceRect: avatarNode.view.bounds,
sourceCornerRadius: avatarNode.view.bounds.width * 0.5,
sourceIsAvatar: false
)
avatarNode.isHidden = true
return transitionIn
} else {
return nil
}
},
transitionOut: { [weak avatarNode] _ in
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
}
},
setFocusedItem: { _ in
},
setProgress: { [weak avatarNode] signal in
guard let avatarNode else {
return
}
let _ = avatarNode.pushLoadingStatus(signal: signal)
}
)
}
static func openPeerStoriesCustom(
context: AccountContext,
peerId: EnginePeer.Id,
isHidden: Bool,
singlePeer: Bool,
parentController: ViewController,
transitionIn: @escaping () -> StoryContainerScreen.TransitionIn?,
transitionOut: @escaping (EnginePeer.Id) -> StoryContainerScreen.TransitionOut?,
setFocusedItem: @escaping (Signal<StoryId?, NoError>) -> Void,
setProgress: @escaping (Signal<Never, NoError>) -> Void
) {
let storyContent = StoryContentContextImpl(context: context, isHidden: isHidden, focusedPeerId: peerId, singlePeer: singlePeer)
let signal = storyContent.state
|> take(1)
|> mapToSignal { state -> Signal<StoryContentContextState, NoError> in
if let slice = state.slice {
return waitUntilStoryMediaPreloaded(context: context, peerId: slice.peer.id, storyItem: slice.item.storyItem)
|> timeout(2.0, queue: .mainQueue(), alternate: .complete())
|> map { _ -> StoryContentContextState in
}
|> then(.single(state))
} else {
return .single(state)
}
}
|> deliverOnMainQueue
|> map { [weak parentController] state -> Void in
if state.slice == nil {
return
}
let transitionIn: StoryContainerScreen.TransitionIn? = transitionIn()
let storyContainerScreen = StoryContainerScreen(
context: context,
content: storyContent,
transitionIn: transitionIn,
transitionOut: { peerId, _ in
return transitionOut(peerId)
}
)
setFocusedItem(storyContainerScreen.focusedItem)
parentController?.push(storyContainerScreen)
}
|> ignoreValues
setProgress(signal)
}
}

View File

@ -7,6 +7,7 @@ import AccountContext
import TelegramCore
import Postbox
import MediaResources
import RangeSet
private struct StoryKey: Hashable {
var peerId: EnginePeer.Id
@ -746,6 +747,8 @@ public final class StoryContentContextImpl: StoryContentContext {
})
}
}
} else {
self.updateState()
}
}
@ -1405,6 +1408,88 @@ public func preloadStoryMedia(context: AccountContext, peer: PeerReference, stor
return combineLatest(signals) |> ignoreValues
}
public func waitUntilStoryMediaPreloaded(context: AccountContext, peerId: EnginePeer.Id, storyItem: EngineStoryItem) -> Signal<Never, NoError> {
return context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
)
|> mapToSignal { peerValue -> Signal<Never, NoError> in
guard let peerValue else {
return .complete()
}
guard let peer = PeerReference(peerValue._asPeer()) else {
return .complete()
}
var statusSignals: [Signal<Never, NoError>] = []
var loadSignals: [Signal<Never, NoError>] = []
switch storyItem.media {
case let .image(image):
if let representation = largestImageRepresentation(image.representations) {
statusSignals.append(
context.account.postbox.mediaBox.resourceData(representation.resource)
|> filter { data in
return data.complete
}
|> take(1)
|> ignoreValues
)
loadSignals.append(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(peer.id), userContentType: .other, reference: .media(media: .story(peer: peer, id: storyItem.id, media: storyItem.media._asMedia()), resource: representation.resource), range: nil)
|> ignoreValues
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
})
}
case let .file(file):
var fetchRange: (Range<Int64>, MediaBoxFetchPriority)?
for attribute in file.attributes {
if case let .Video(_, _, _, preloadSize) = attribute {
if let preloadSize {
fetchRange = (0 ..< Int64(preloadSize), .default)
}
break
}
}
statusSignals.append(
context.account.postbox.mediaBox.resourceRangesStatus(file.resource)
|> filter { ranges in
if let fetchRange {
return ranges.isSuperset(of: RangeSet(fetchRange.0))
} else {
return true
}
}
|> take(1)
|> ignoreValues
)
loadSignals.append(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(peer.id), userContentType: .other, reference: .media(media: .story(peer: peer, id: storyItem.id, media: storyItem.media._asMedia()), resource: file.resource), range: fetchRange)
|> ignoreValues
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
})
loadSignals.append(context.account.postbox.mediaBox.cachedResourceRepresentation(file.resource, representation: CachedVideoFirstFrameRepresentation(), complete: true, fetch: true, attemptSynchronously: false)
|> ignoreValues)
default:
break
}
return Signal { subscriber in
let statusDisposable = combineLatest(statusSignals).start(completed: {
subscriber.putCompletion()
})
let loadDisposable = combineLatest(loadSignals).start()
return ActionDisposable {
statusDisposable.dispose()
loadDisposable.dispose()
}
}
}
}
func extractItemEntityFiles(item: EngineStoryItem, allEntityFiles: [MediaId: TelegramMediaFile]) -> [MediaId: TelegramMediaFile] {
var result: [MediaId: TelegramMediaFile] = [:]
for entity in item.entities {

View File

@ -1757,4 +1757,3 @@ func allowedStoryReactions(context: AccountContext) -> Signal<[ReactionItem], No
return result
}
}

View File

@ -2068,11 +2068,11 @@ public final class StoryItemSetContainerComponent: Component {
}
self.navigateToPeer(peer: peer, chat: false)
},
openPeerStories: { [weak self] peer, sourceView in
openPeerStories: { [weak self] peer, avatarNode in
guard let self else {
return
}
self.openPeerStories(peer: peer, sourceView: sourceView)
self.openPeerStories(peer: peer, avatarNode: avatarNode)
}
)),
environment: {},
@ -3248,75 +3248,15 @@ public final class StoryItemSetContainerComponent: Component {
}
}
func openPeerStories(peer: EnginePeer, sourceView: UIView) {
func openPeerStories(peer: EnginePeer, avatarNode: AvatarNode) {
guard let component = self.component else {
return
}
guard let controller = component.controller() 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)
})
StoryContainerScreen.openPeerStories(context: component.context, peerId: peer.id, parentController: controller, avatarNode: avatarNode)
}
private func openStoryEditing() {

View File

@ -14,6 +14,7 @@ import ShimmerEffect
import StoryFooterPanelComponent
import PeerListItemComponent
import AnimatedStickerComponent
import AvatarNode
final class StoryItemSetViewListComponent: Component {
final class AnimationHint {
@ -56,7 +57,7 @@ final class StoryItemSetViewListComponent: Component {
let deleteAction: () -> Void
let moreAction: (UIView, ContextGesture?) -> Void
let openPeer: (EnginePeer) -> Void
let openPeerStories: (EnginePeer, UIView) -> Void
let openPeerStories: (EnginePeer, AvatarNode) -> Void
init(
externalState: ExternalState,
@ -75,7 +76,7 @@ final class StoryItemSetViewListComponent: Component {
deleteAction: @escaping () -> Void,
moreAction: @escaping (UIView, ContextGesture?) -> Void,
openPeer: @escaping (EnginePeer) -> Void,
openPeerStories: @escaping (EnginePeer, UIView) -> Void
openPeerStories: @escaping (EnginePeer, AvatarNode) -> Void
) {
self.externalState = externalState
self.context = context
@ -499,11 +500,11 @@ final class StoryItemSetViewListComponent: Component {
}
component.openPeer(peer)
},
openStories: { [weak self] peer, sourceView in
openStories: { [weak self] peer, avatarNode in
guard let self, let component = self.component else {
return
}
component.openPeerStories(peer, sourceView)
component.openPeerStories(peer, avatarNode)
}
)),
environment: {},

View File

@ -338,6 +338,9 @@ public final class StoryPeerListComponent: Component {
private var previewedItemDisposable: Disposable?
private var previewedItemId: EnginePeer.Id?
private var loadingItemDisposable: Disposable?
private var loadingItemId: EnginePeer.Id?
private var animationState: AnimationState?
private var animator: ConstantDisplayLinkAnimator?
@ -403,6 +406,7 @@ public final class StoryPeerListComponent: Component {
deinit {
self.loadMoreDisposable.dispose()
self.previewedItemDisposable?.dispose()
self.loadingItemDisposable?.dispose()
}
@objc private func collapsedButtonPressed() {
@ -444,6 +448,22 @@ public final class StoryPeerListComponent: Component {
})
}
public func setLoadingItem(peerId: EnginePeer.Id, signal: Signal<Never, NoError>) {
self.loadingItemId = peerId
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2, execute: { [weak self] in
self?.state?.updated(transition: .immediate)
})
self.loadingItemDisposable?.dispose()
self.loadingItemDisposable = (signal |> deliverOnMainQueue).start(completed: { [weak self] in
guard let self else {
return
}
self.loadingItemId = nil
self.state?.updated(transition: .immediate)
})
}
public func anchorForTooltip() -> (UIView, CGRect)? {
return (self.collapsedButton, self.collapsedButton.bounds)
}
@ -869,8 +889,9 @@ public final class StoryPeerListComponent: Component {
}
hasUnseenCloseFriendsItems = false
} else if peer.id == self.loadingItemId {
itemRingAnimation = .loading
}
//itemRingAnimation = .loading
let measuredItem = calculateItem(i)

View File

@ -17033,110 +17033,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
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, weak avatarNode] _ 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
} 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(
context: self.context,
content: storyContent,
transitionIn: transitionIn,
transitionOut: { peerId, _ in
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
}
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)
})
if let avatarNode = avatarHeaderNode?.avatarNode ?? avatarNode {
StoryContainerScreen.openPeerStories(context: self.context, peerId: peerId, parentController: self, avatarNode: avatarNode)
}
}
private func openPeerMention(_ name: String, navigation: ChatControllerInteractionNavigateToPeer = .default, sourceMessageId: MessageId? = nil) {

View File

@ -2073,6 +2073,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
var downloadableResourceIds: [(messageId: MessageId, resourceId: String)] = []
var allVisibleAnchorMessageIds: [(MessageId, Int)] = []
var visibleAdOpaqueIds: [Data] = []
var peerIdsWithRefreshStories: [PeerId] = []
if indexRange.0 <= indexRange.1 {
for i in (indexRange.0 ... indexRange.1) {
@ -2080,6 +2081,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
switch historyView.filteredEntries[i] {
case let .MessageEntry(message, _, _, _, _, _):
if let author = message.author as? TelegramUser {
peerIdsWithRefreshStories.append(author.id)
}
var hasUnconsumedMention = false
var hasUnconsumedContent = false
if message.tags.contains(.unseenPersonalMessage) {
@ -2187,6 +2192,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
allVisibleAnchorMessageIds.append((message.id, nodeIndex))
}
case let .MessageGroupEntry(_, messages, _):
if let author = messages.first?.0.author as? TelegramUser {
peerIdsWithRefreshStories.append(author.id)
}
for (message, _, _, _, _) in messages {
var hasUnconsumedMention = false
var hasUnconsumedContent = false
@ -2393,6 +2402,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
self.markAdAsSeen(opaqueId: opaqueId)
}
}
if !peerIdsWithRefreshStories.isEmpty {
self.context.account.viewTracker.refreshStoryStatsForPeerIds(peerIds: peerIdsWithRefreshStories)
}
self.currentEarlierPrefetchMessages = toEarlierMediaMessages
self.currentLaterPrefetchMessages = toLaterMediaMessages

View File

@ -4107,7 +4107,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
private func openStories(fromAvatar: Bool) {
guard let controller = self.controller else {
return
}
if let expiringStoryList = self.expiringStoryList, let expiringStoryListState = self.expiringStoryListState, !expiringStoryListState.items.isEmpty {
if fromAvatar {
StoryContainerScreen.openPeerStories(context: self.context, peerId: self.peerId, parentController: controller, avatarNode: self.headerNode.avatarListNode.avatarContainerNode.avatarNode)
}
let _ = expiringStoryList
let storyContent = StoryContentContextImpl(context: self.context, isHidden: false, focusedPeerId: self.peerId, singlePeer: true)
let _ = (storyContent.state
@ -7141,6 +7148,13 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
case .remove:
data.members?.membersContext.removeMember(memberId: member.id)
case let .openStories(sourceView):
guard let controller = self.controller else {
return
}
if let avatarNode = sourceView.asyncdisplaykit_node as? AvatarNode {
StoryContainerScreen.openPeerStories(context: self.context, peerId: member.id, parentController: controller, avatarNode: avatarNode)
return
}
let storyContent = StoryContentContextImpl(context: self.context, isHidden: false, focusedPeerId: member.id, singlePeer: true)
let _ = (storyContent.state
|> filter { $0.slice != nil }