mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Stories
This commit is contained in:
parent
9672c77070
commit
6d7f74ecc3
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ swift_library(
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/Components/HierarchyTrackingLayer",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -76,6 +76,7 @@ swift_library(
|
||||
"//submodules/OpenInExternalAppUI",
|
||||
"//submodules/MediaPasteboardUI",
|
||||
"//submodules/WebPBinding",
|
||||
"//submodules/Utils/RangeSet",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -1757,4 +1757,3 @@ func allowedStoryReactions(context: AccountContext) -> Signal<[ReactionItem], No
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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: {},
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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 }
|
||||
|
Loading…
x
Reference in New Issue
Block a user