From a11401b352956f313c15d7cf65957b084a80c985 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 21 Jul 2023 19:58:19 +0400 Subject: [PATCH] Stories --- .../Telegram-iOS/en.lproj/Localizable.strings | 2 + .../Sources/Node/ChatListItem.swift | 2 + .../Sources/SparseItemGrid.swift | 11 +- .../Sources/Account/Account.swift | 1 + .../Sources/Account/AccountManager.swift | 1 + .../State/AccountStateManagementUtils.swift | 18 +++ ...agedSynchronizePeerStoriesOperations.swift | 121 ++++++++++++++++++ .../SyncCore/SyncCore_Namespaces.swift | 1 + .../SynchronizePeerStoriesOperation.swift | 32 +++++ .../Contacts/ContactManagement.swift | 4 + .../Messages/StoryListContext.swift | 6 +- .../TelegramCore/Sources/UpdatePeers.swift | 111 ++++++++++++---- .../Sources/PeerInfoStoryGridScreen.swift | 14 ++ .../Sources/PeerInfoStoryPaneNode.swift | 50 +++++--- .../Sources/PeerInfoVisualMediaPaneNode.swift | 4 +- .../Sources/StoryChatContent.swift | 12 +- .../StoryItemSetContainerComponent.swift | 15 ++- ...StoryItemSetContainerViewSendMessage.swift | 28 +++- .../Sources/StoryPeerListComponent.swift | 46 +++++-- .../Sources/StoryPeerListItemComponent.swift | 14 +- .../ChatMessageWebpageBubbleContentNode.swift | 2 +- 21 files changed, 417 insertions(+), 78 deletions(-) create mode 100644 submodules/TelegramCore/Sources/State/ManagedSynchronizePeerStoriesOperations.swift create mode 100644 submodules/TelegramCore/Sources/SyncCore/SynchronizePeerStoriesOperation.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 705a73994b..b9fd01475b 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -9716,3 +9716,5 @@ Sorry for the inconvenience."; "MediaEditor.Draft" = "Draft"; "Notification.LockScreenStoryPlaceholder" = "New Story"; + +"Chat.OpenStory" = "OPEN STORY"; diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 8b481e6c1b..bd7a1156a3 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -3601,6 +3601,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } else { strongSelf.view.accessibilityCustomActions = nil } + + strongSelf.avatarTapRecognizer?.isEnabled = item.interaction.inlineNavigationLocation == nil } }) } diff --git a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift index 19fd2c372e..b003061a4e 100644 --- a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift +++ b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift @@ -8,7 +8,7 @@ import ComponentFlow import MultilineTextComponent public protocol SparseItemGridLayer: CALayer { - func update(size: CGSize) + func update(size: CGSize, insets: UIEdgeInsets, displayItem: SparseItemGridDisplayItem, binding: SparseItemGridBinding, item: SparseItemGrid.Item?) func needsShimmer() -> Bool func getContents() -> Any? @@ -967,7 +967,7 @@ public final class SparseItemGrid: ASDisplayNode { var bindItems: [Item] = [] var bindLayers: [SparseItemGridDisplayItem] = [] - var updateLayers: [SparseItemGridDisplayItem] = [] + var updateLayers: [(SparseItemGridDisplayItem, Int)] = [] let addBlur = layout.centerItems @@ -980,7 +980,7 @@ public final class SparseItemGrid: ASDisplayNode { let itemLayer: VisibleItem if let current = self.visibleItems[item.id] { itemLayer = current - updateLayers.append(itemLayer) + updateLayers.append((itemLayer, index)) } else { itemLayer = VisibleItem(layer: items.itemBinding.createLayer(), view: items.itemBinding.createView()) self.visibleItems[item.id] = itemLayer @@ -1057,10 +1057,11 @@ public final class SparseItemGrid: ASDisplayNode { items.itemBinding.bindLayers(items: bindItems, layers: bindLayers, size: layout.containerLayout.size, insets: layout.containerLayout.insets, synchronous: synchronous) } - for item in updateLayers { + for (item, index) in updateLayers { let item = item as! VisibleItem + let contentItem = items.item(at: index) if let layer = item.layer { - layer.update(size: layer.frame.size) + layer.update(size: layer.frame.size, insets: layout.containerLayout.insets, displayItem: item, binding: items.itemBinding, item: contentItem) } else if let view = item.view { view.update(size: view.layer.frame.size, insets: layout.containerLayout.insets) } diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index 284a267ca9..8bb8f2e23f 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -1158,6 +1158,7 @@ public class Account { self.managedOperationsDisposable.add(managedAutoexpireStoryOperations(network: self.network, postbox: self.postbox).start()) self.managedOperationsDisposable.add(managedPeerTimestampAttributeOperations(network: self.network, postbox: self.postbox).start()) self.managedOperationsDisposable.add(managedSynchronizeViewStoriesOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) + self.managedOperationsDisposable.add(managedSynchronizePeerStoriesOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) self.managedOperationsDisposable.add(managedLocalTypingActivities(activities: self.localInputActivityManager.allActivities(), postbox: self.stateManager.postbox, network: self.stateManager.network, accountPeerId: self.stateManager.accountPeerId).start()) let extractedExpr: [Signal] = [ diff --git a/submodules/TelegramCore/Sources/Account/AccountManager.swift b/submodules/TelegramCore/Sources/Account/AccountManager.swift index 1e47a0d370..c94b3ec025 100644 --- a/submodules/TelegramCore/Sources/Account/AccountManager.swift +++ b/submodules/TelegramCore/Sources/Account/AccountManager.swift @@ -197,6 +197,7 @@ private var declaredEncodables: Void = { declareEncodable(SynchronizeAutosaveItemOperation.self, f: { SynchronizeAutosaveItemOperation(decoder: $0) }) declareEncodable(TelegramMediaStory.self, f: { TelegramMediaStory(decoder: $0) }) declareEncodable(SynchronizeViewStoriesOperation.self, f: { SynchronizeViewStoriesOperation(decoder: $0) }) + declareEncodable(SynchronizePeerStoriesOperation.self, f: { SynchronizePeerStoriesOperation(decoder: $0) }) return }() diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index e10e1dfb5f..63a2ecd68b 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -4489,6 +4489,11 @@ func replayFinalState( updatedPeerEntries.append(StoryItemsTableEntry(value: codedEntry, id: storedItem.id, expirationTimestamp: storedItem.expirationTimestamp, isCloseFriends: storedItem.isCloseFriends)) } } + if case .item = storedItem { + if let codedEntry = CodableEntry(storedItem) { + transaction.setStory(id: StoryId(peerId: peerId, id: storedItem.id), value: codedEntry) + } + } } else { if case let .storyItemDeleted(id) = story { if let index = updatedPeerEntries.firstIndex(where: { $0.id == id }) { @@ -4981,5 +4986,18 @@ func replayFinalState( requestChatListFiltersSync(transaction: transaction) } + for update in storyUpdates { + switch update { + case let .added(peerId, _): + if shouldKeepUserStoriesInFeed(peerId: peerId, isContact: transaction.isPeerContact(peerId: peerId)) { + if !transaction.storySubscriptionsContains(key: .hidden, peerId: peerId) && !transaction.storySubscriptionsContains(key: .filtered, peerId: peerId) { + _internal_addSynchronizePeerStoriesOperation(peerId: peerId, transaction: transaction) + } + } + default: + break + } + } + return AccountReplayedFinalState(state: finalState, addedIncomingMessageIds: addedIncomingMessageIds, addedReactionEvents: addedReactionEvents, wasScheduledMessageIds: wasScheduledMessageIds, addedSecretMessageIds: addedSecretMessageIds, deletedMessageIds: deletedMessageIds, updatedTypingActivities: updatedTypingActivities, updatedWebpages: updatedWebpages, updatedCalls: updatedCalls, addedCallSignalingData: addedCallSignalingData, updatedGroupCallParticipants: updatedGroupCallParticipants, storyUpdates: storyUpdates, updatedPeersNearby: updatedPeersNearby, isContactUpdates: isContactUpdates, delayNotificatonsUntil: delayNotificatonsUntil, updatedIncomingThreadReadStates: updatedIncomingThreadReadStates, updatedOutgoingThreadReadStates: updatedOutgoingThreadReadStates, updateConfig: updateConfig, isPremiumUpdated: isPremiumUpdated) } diff --git a/submodules/TelegramCore/Sources/State/ManagedSynchronizePeerStoriesOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSynchronizePeerStoriesOperations.swift new file mode 100644 index 0000000000..7da8510625 --- /dev/null +++ b/submodules/TelegramCore/Sources/State/ManagedSynchronizePeerStoriesOperations.swift @@ -0,0 +1,121 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi +import MtProtoKit + +private final class ManagedSynchronizePeerStoriesOperationsHelper { + var operationDisposables: [PeerId: Disposable] = [:] + + func update(_ entries: [PeerMergedOperationLogEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) { + var disposeOperations: [Disposable] = [] + var beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)] = [] + + var validPeerIds: [PeerId] = [] + for entry in entries { + guard let _ = entry.contents as? SynchronizePeerStoriesOperation else { + continue + } + validPeerIds.append(entry.peerId) + var replace = true + if let _ = self.operationDisposables[entry.peerId] { + } else { + replace = true + } + if replace { + let disposable = MetaDisposable() + self.operationDisposables[entry.peerId] = disposable + beginOperations.append((entry, disposable)) + } + } + + var removedPeerIds: [PeerId] = [] + for (peerId, info) in self.operationDisposables { + if !validPeerIds.contains(peerId) { + removedPeerIds.append(peerId) + disposeOperations.append(info) + } + } + for peerId in removedPeerIds { + self.operationDisposables.removeValue(forKey: peerId) + } + + return (disposeOperations, beginOperations) + } + + func reset() -> [Disposable] { + let disposables = Array(self.operationDisposables.values) + self.operationDisposables.removeAll() + return disposables + } +} + +private func withTakenOperation(postbox: Postbox, peerId: PeerId, tagLocalIndex: Int32, _ f: @escaping (Transaction, PeerMergedOperationLogEntry?) -> Signal) -> Signal { + return postbox.transaction { transaction -> Signal in + var result: PeerMergedOperationLogEntry? + transaction.operationLogUpdateEntry(peerId: peerId, tag: OperationLogTags.SynchronizePeerStories, tagLocalIndex: tagLocalIndex, { entry in + if let entry = entry, let _ = entry.mergedIndex, entry.contents is SynchronizePeerStoriesOperation { + result = entry.mergedEntry! + return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none) + } else { + return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none) + } + }) + + return f(transaction, result) + } |> switchToLatest +} + +func managedSynchronizePeerStoriesOperations(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal { + return Signal { _ in + let helper = Atomic(value: ManagedSynchronizePeerStoriesOperationsHelper()) + + let disposable = postbox.mergedOperationLogView(tag: OperationLogTags.SynchronizePeerStories, limit: 10).start(next: { view in + let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) in + return helper.update(view.entries) + } + + for disposable in disposeOperations { + disposable.dispose() + } + + for (entry, disposable) in beginOperations { + let signal = withTakenOperation(postbox: postbox, peerId: entry.peerId, tagLocalIndex: entry.tagLocalIndex, { transaction, entry -> Signal in + if let entry = entry { + if let operation = entry.contents as? SynchronizePeerStoriesOperation { + if let peer = transaction.getPeer(entry.peerId) { + return pushStoriesAreSeen(postbox: postbox, network: network, stateManager: stateManager, peer: peer, operation: operation) + } else { + return .complete() + } + } else { + assertionFailure() + } + } + return .complete() + }) + |> then(postbox.transaction { transaction -> Void in + let _ = transaction.operationLogRemoveEntry(peerId: entry.peerId, tag: OperationLogTags.SynchronizePeerStories, tagLocalIndex: entry.tagLocalIndex) + }) + + disposable.set(signal.start()) + } + }) + + return ActionDisposable { + let disposables = helper.with { helper -> [Disposable] in + return helper.reset() + } + for disposable in disposables { + disposable.dispose() + } + disposable.dispose() + } + } +} + +private func pushStoriesAreSeen(postbox: Postbox, network: Network, stateManager: AccountStateManager, peer: Peer, operation: SynchronizePeerStoriesOperation) -> Signal { + return _internal_pollPeerStories(postbox: postbox, network: network, accountPeerId: stateManager.accountPeerId, peerId: peer.id, peerReference: PeerReference(peer)) + |> map { _ -> Void in + } +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 353b664c67..d2ec5c083d 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -186,6 +186,7 @@ public struct OperationLogTags { public static let SynchronizeInstalledEmoji = PeerOperationLogTag(value: 22) public static let SynchronizeAutosaveItems = PeerOperationLogTag(value: 23) public static let SynchronizeViewStories = PeerOperationLogTag(value: 24) + public static let SynchronizePeerStories = PeerOperationLogTag(value: 25) } public struct LegacyPeerSummaryCounterTags: OptionSet, Sequence, Hashable { diff --git a/submodules/TelegramCore/Sources/SyncCore/SynchronizePeerStoriesOperation.swift b/submodules/TelegramCore/Sources/SyncCore/SynchronizePeerStoriesOperation.swift new file mode 100644 index 0000000000..fa3713009a --- /dev/null +++ b/submodules/TelegramCore/Sources/SyncCore/SynchronizePeerStoriesOperation.swift @@ -0,0 +1,32 @@ +import Foundation +import Postbox + +public final class SynchronizePeerStoriesOperation: PostboxCoding { + public init() { + } + + public init(decoder: PostboxDecoder) { + } + + public func encode(_ encoder: PostboxEncoder) { + } +} + +func _internal_addSynchronizePeerStoriesOperation(peerId: PeerId, transaction: Transaction) { + let tag: PeerOperationLogTag = OperationLogTags.SynchronizePeerStories + + var topOperation: (SynchronizePeerStoriesOperation, Int32)? + transaction.operationLogEnumerateEntries(peerId: peerId, tag: tag, { entry in + if let operation = entry.contents as? SynchronizePeerStoriesOperation { + topOperation = (operation, entry.tagLocalIndex) + } + return false + }) + var replace = false + if topOperation == nil { + replace = true + } + if replace { + transaction.operationLogAddEntry(peerId: peerId, tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SynchronizePeerStoriesOperation()) + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Contacts/ContactManagement.swift b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/ContactManagement.swift index 14d7948a78..b009f95ebf 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Contacts/ContactManagement.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/ContactManagement.swift @@ -108,6 +108,10 @@ func _internal_deleteContactPeerInteractively(account: Account, peerId: PeerId) account.stateManager.addUpdates(updates) } return account.postbox.transaction { transaction -> Void in + if let user = peer as? TelegramUser { + _internal_updatePeerIsContact(transaction: transaction, user: user, isContact: false) + } + var peerIds = transaction.getContactPeerIds() if peerIds.contains(peerId) { peerIds.remove(peerId) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index 931a126e7b..c311fa994c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -1187,9 +1187,9 @@ public final class PeerExpiringStoryListContext { } } -public func _internal_pollPeerStories(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId) -> Signal { +public func _internal_pollPeerStories(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, peerReference: PeerReference? = nil) -> Signal { return postbox.transaction { transaction -> Api.InputUser? in - return transaction.getPeer(peerId).flatMap(apiInputUser) + return transaction.getPeer(peerId).flatMap(apiInputUser) ?? peerReference?.inputUser } |> mapToSignal { inputUser -> Signal in guard let inputUser = inputUser else { @@ -1236,7 +1236,7 @@ public func _internal_pollPeerStories(postbox: Postbox, network: Network, accoun transaction.setStoryItems(peerId: peerId, items: updatedPeerEntries) - if !updatedPeerEntries.isEmpty { + if !updatedPeerEntries.isEmpty, shouldKeepUserStoriesInFeed(peerId: peerId, isContact: transaction.isPeerContact(peerId: peerId)) { if let user = transaction.getPeer(peerId) as? TelegramUser, let storiesHidden = user.storiesHidden { if storiesHidden { if !transaction.storySubscriptionsContains(key: .hidden, peerId: peerId) { diff --git a/submodules/TelegramCore/Sources/UpdatePeers.swift b/submodules/TelegramCore/Sources/UpdatePeers.swift index a2dd3ffa05..1715df1ba8 100644 --- a/submodules/TelegramCore/Sources/UpdatePeers.swift +++ b/submodules/TelegramCore/Sources/UpdatePeers.swift @@ -38,6 +38,13 @@ func minTimestampForPeerInclusion(_ peer: Peer) -> Int32? { } } +func shouldKeepUserStoriesInFeed(peerId: PeerId, isContact: Bool) -> Bool { + if peerId.namespace == Namespaces.Peer.CloudUser && (peerId.id._internalGetInt64Value() == 777000 || peerId.id._internalGetInt64Value() == 333000) { + return true + } + return isContact +} + func updatePeers(transaction: Transaction, accountPeerId: PeerId, peers: AccumulatedPeers) { var parsedPeers: [Peer] = [] for (_, user) in peers.users { @@ -47,11 +54,17 @@ func updatePeers(transaction: Transaction, accountPeerId: PeerId, peers: Accumul case let .user(flags, flags2, _, _, _, _, _, _, _, _, _, _, _, _, _, _, storiesMaxId): let isMin = (flags & (1 << 20)) != 0 let storiesUnavailable = (flags2 & (1 << 4)) != 0 + if let storiesMaxId = storiesMaxId { transaction.setStoryItemsInexactMaxId(peerId: user.peerId, id: storiesMaxId) } else if !isMin && storiesUnavailable { transaction.clearStoryItemsInexactMaxId(peerId: user.peerId) } + + if !isMin { + let isContact = (flags & (1 << 11)) != 0 + _internal_updatePeerIsContact(transaction: transaction, user: telegramUser, isContact: isContact) + } case .userEmpty: break } @@ -65,6 +78,54 @@ func updatePeers(transaction: Transaction, accountPeerId: PeerId, peers: Accumul updatePeerPresences(transaction: transaction, accountPeerId: accountPeerId, peerPresences: peers.users) } +func _internal_updatePeerIsContact(transaction: Transaction, user: TelegramUser, isContact: Bool) { + let previousValue = shouldKeepUserStoriesInFeed(peerId: user.id, isContact: transaction.isPeerContact(peerId: user.id)) + let updatedValue = shouldKeepUserStoriesInFeed(peerId: user.id, isContact: isContact) + + if previousValue != updatedValue, let storiesHidden = user.storiesHidden { + if updatedValue { + if storiesHidden { + if transaction.storySubscriptionsContains(key: .filtered, peerId: user.id) { + var (state, peerIds) = transaction.getAllStorySubscriptions(key: .filtered) + peerIds.removeAll(where: { $0 == user.id }) + transaction.replaceAllStorySubscriptions(key: .filtered, state: state, peerIds: peerIds) + } + if !transaction.storySubscriptionsContains(key: .hidden, peerId: user.id) { + var (state, peerIds) = transaction.getAllStorySubscriptions(key: .hidden) + if !peerIds.contains(user.id) { + peerIds.append(user.id) + transaction.replaceAllStorySubscriptions(key: .hidden, state: state, peerIds: peerIds) + } + } + } else { + if transaction.storySubscriptionsContains(key: .hidden, peerId: user.id) { + var (state, peerIds) = transaction.getAllStorySubscriptions(key: .hidden) + peerIds.removeAll(where: { $0 == user.id }) + transaction.replaceAllStorySubscriptions(key: .hidden, state: state, peerIds: peerIds) + } + if !transaction.storySubscriptionsContains(key: .filtered, peerId: user.id) { + var (state, peerIds) = transaction.getAllStorySubscriptions(key: .filtered) + if !peerIds.contains(user.id) { + peerIds.append(user.id) + transaction.replaceAllStorySubscriptions(key: .filtered, state: state, peerIds: peerIds) + } + } + } + } else { + if transaction.storySubscriptionsContains(key: .filtered, peerId: user.id) { + var (state, peerIds) = transaction.getAllStorySubscriptions(key: .filtered) + peerIds.removeAll(where: { $0 == user.id }) + transaction.replaceAllStorySubscriptions(key: .filtered, state: state, peerIds: peerIds) + } + if transaction.storySubscriptionsContains(key: .hidden, peerId: user.id) { + var (state, peerIds) = transaction.getAllStorySubscriptions(key: .hidden) + peerIds.removeAll(where: { $0 == user.id }) + transaction.replaceAllStorySubscriptions(key: .hidden, state: state, peerIds: peerIds) + } + } + } +} + public func updatePeersCustom(transaction: Transaction, peers: [Peer], update: (Peer?, Peer) -> Peer?) { transaction.updatePeersInternal(peers, update: { previous, updated in let peerId = updated.id @@ -79,32 +140,34 @@ public func updatePeersCustom(transaction: Transaction, peers: [Peer], update: ( switch peerId.namespace { case Namespaces.Peer.CloudUser: - if let updated = updated as? TelegramUser, let previous = previous as? TelegramUser, let storiesHidden = updated.storiesHidden, storiesHidden != previous.storiesHidden { - if storiesHidden { - if transaction.storySubscriptionsContains(key: .filtered, peerId: updated.id) { - var (state, peerIds) = transaction.getAllStorySubscriptions(key: .filtered) - peerIds.removeAll(where: { $0 == updated.id }) - transaction.replaceAllStorySubscriptions(key: .filtered, state: state, peerIds: peerIds) - - if !transaction.storySubscriptionsContains(key: .hidden, peerId: updated.id) { - var (state, peerIds) = transaction.getAllStorySubscriptions(key: .hidden) - if !peerIds.contains(updated.id) { - peerIds.append(updated.id) - transaction.replaceAllStorySubscriptions(key: .hidden, state: state, peerIds: peerIds) + if let updated = updated as? TelegramUser, let previous = previous as? TelegramUser { + if let storiesHidden = updated.storiesHidden, storiesHidden != previous.storiesHidden { + if storiesHidden { + if transaction.storySubscriptionsContains(key: .filtered, peerId: updated.id) { + var (state, peerIds) = transaction.getAllStorySubscriptions(key: .filtered) + peerIds.removeAll(where: { $0 == updated.id }) + transaction.replaceAllStorySubscriptions(key: .filtered, state: state, peerIds: peerIds) + + if !transaction.storySubscriptionsContains(key: .hidden, peerId: updated.id) { + var (state, peerIds) = transaction.getAllStorySubscriptions(key: .hidden) + if !peerIds.contains(updated.id) { + peerIds.append(updated.id) + transaction.replaceAllStorySubscriptions(key: .hidden, state: state, peerIds: peerIds) + } } } - } - } else { - if transaction.storySubscriptionsContains(key: .hidden, peerId: updated.id) { - var (state, peerIds) = transaction.getAllStorySubscriptions(key: .hidden) - peerIds.removeAll(where: { $0 == updated.id }) - transaction.replaceAllStorySubscriptions(key: .hidden, state: state, peerIds: peerIds) - - if !transaction.storySubscriptionsContains(key: .filtered, peerId: updated.id) { - var (state, peerIds) = transaction.getAllStorySubscriptions(key: .filtered) - if !peerIds.contains(updated.id) { - peerIds.append(updated.id) - transaction.replaceAllStorySubscriptions(key: .filtered, state: state, peerIds: peerIds) + } else { + if transaction.storySubscriptionsContains(key: .hidden, peerId: updated.id) { + var (state, peerIds) = transaction.getAllStorySubscriptions(key: .hidden) + peerIds.removeAll(where: { $0 == updated.id }) + transaction.replaceAllStorySubscriptions(key: .hidden, state: state, peerIds: peerIds) + + if !transaction.storySubscriptionsContains(key: .filtered, peerId: updated.id) { + var (state, peerIds) = transaction.getAllStorySubscriptions(key: .filtered) + if !peerIds.contains(updated.id) { + peerIds.append(updated.id) + transaction.replaceAllStorySubscriptions(key: .filtered, state: state, peerIds: peerIds) + } } } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift index 5aae15186d..bc21449495 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift @@ -270,6 +270,13 @@ final class PeerInfoStoryGridScreenComponent: Component { }) } + func scrollToTop() { + guard let paneNode = self.paneNode else { + return + } + let _ = paneNode.scrollToTop() + } + func update(component: PeerInfoStoryGridScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.component = component self.state = state @@ -500,6 +507,13 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer { self.navigationItem.titleView = self.titleView self.updateTitle() + + self.scrollToTop = { [weak self] in + guard let self, let componentView = self.node.hostView.componentView as? PeerInfoStoryGridScreenComponent.View else { + return + } + componentView.scrollToTop() + } } required public init(coder aDecoder: NSCoder) { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 1c317bad07..0a7ba25b6e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -329,10 +329,14 @@ private final class GenericItemLayer: CALayer, ItemLayer { return !self.hasContents } - func update(size: CGSize) { - /*if let durationLayer = self.durationLayer { + func update(size: CGSize, insets: UIEdgeInsets, displayItem: SparseItemGridDisplayItem, binding: SparseItemGridBinding, item: SparseItemGrid.Item?) { + if let durationLayer = self.durationLayer { durationLayer.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 3.0), size: CGSize()) - }*/ + } + + if let binding = binding as? SparseItemGridBindingImpl, let item = item as? VisualMediaItem, let previousItem = self.item, previousItem.story.media.id != item.story.media.id { + binding.bindLayers(items: [item], layers: [displayItem], size: size, insets: insets, synchronous: .none) + } } } @@ -469,10 +473,10 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL return !self.hasContents } - func update(size: CGSize) { - /*if let durationLayer = self.durationLayer { + func update(size: CGSize, insets: UIEdgeInsets, displayItem: SparseItemGridDisplayItem, binding: SparseItemGridBinding, item: SparseItemGrid.Item?) { + if let durationLayer = self.durationLayer { durationLayer.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 3.0), size: CGSize()) - }*/ + } } } @@ -1229,7 +1233,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } self.itemGridBinding.itemInteraction = self._itemInteraction - self.contextGestureContainerNode.isGestureEnabled = true + self.contextGestureContainerNode.isGestureEnabled = false self.contextGestureContainerNode.addSubnode(self.itemGrid) self.addSubnode(self.contextGestureContainerNode) @@ -1631,6 +1635,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr gridSnapshot = self.itemGrid.view.snapshotView(afterScreenUpdates: false) } self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: false, transition: .immediate) + self.updateSelectedItems(animated: false) if let gridSnapshot = gridSnapshot { self.view.addSubview(gridSnapshot) gridSnapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak gridSnapshot] _ in @@ -1733,16 +1738,16 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr self.itemGrid.addToTransitionSurface(view: view) } - private var gridSelectionGesture: MediaPickerGridSelectionGesture? + private var gridSelectionGesture: MediaPickerGridSelectionGesture? override public func didLoad() { super.didLoad() - let selectionRecognizer = MediaListSelectionRecognizer(target: self, action: #selector(self.selectionPanGesture(_:))) + /*let selectionRecognizer = MediaListSelectionRecognizer(target: self, action: #selector(self.selectionPanGesture(_:))) selectionRecognizer.shouldBegin = { return true } - self.view.addGestureRecognizer(selectionRecognizer) + self.view.addGestureRecognizer(selectionRecognizer)*/ } private var selectionPanState: (selecting: Bool, initialMessageId: EngineMessage.Id, toggledMessageIds: [[EngineMessage.Id]])? @@ -1807,17 +1812,18 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } override public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - let location = gestureRecognizer.location(in: gestureRecognizer.view) + /*let location = gestureRecognizer.location(in: gestureRecognizer.view) if location.x < 44.0 { return false - } + }*/ return true } public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { if gestureRecognizer.state != .failed, let otherGestureRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer { - otherGestureRecognizer.isEnabled = false - otherGestureRecognizer.isEnabled = true + let _ = otherGestureRecognizer + //otherGestureRecognizer.isEnabled = false + //otherGestureRecognizer.isEnabled = true return true } else { return false @@ -1841,27 +1847,29 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr itemLayer.updateSelection(theme: self.itemGridBinding.checkNodeTheme, isSelected: self.itemInteraction.selectedIds?.contains(item.story.id), animated: animated) } - /*let isSelecting = self.chatControllerInteraction.selectionState != nil + let isSelecting = self._itemInteraction?.selectedIds != nil self.itemGrid.pinchEnabled = !isSelecting + self.view.disablesInteractiveTransitionGestureRecognizer = isSelecting + if isSelecting { if self.gridSelectionGesture == nil { - let selectionGesture = MediaPickerGridSelectionGesture() + let selectionGesture = MediaPickerGridSelectionGesture() selectionGesture.delegate = self selectionGesture.sideInset = 44.0 selectionGesture.updateIsScrollEnabled = { [weak self] isEnabled in self?.itemGrid.isScrollEnabled = isEnabled } selectionGesture.itemAt = { [weak self] point in - if let strongSelf = self, let itemLayer = strongSelf.itemGrid.item(at: point)?.layer as? ItemLayer, let messageId = itemLayer.item?.message.id { - return (messageId, strongSelf.chatControllerInteraction.selectionState?.selectedIds.contains(messageId) ?? false) + if let strongSelf = self, let itemLayer = strongSelf.itemGrid.item(at: point)?.layer as? ItemLayer, let storyId = itemLayer.item?.story.id { + return (storyId, strongSelf._itemInteraction?.selectedIds?.contains(storyId) ?? false) } else { return nil } } - selectionGesture.updateSelection = { [weak self] messageId, selected in + selectionGesture.updateSelection = { [weak self] storyId, selected in if let strongSelf = self { - strongSelf.chatControllerInteraction.toggleMessagesSelection([messageId], selected) + strongSelf._itemInteraction?.toggleSelection(storyId, selected) } } self.itemGrid.view.addGestureRecognizer(selectionGesture) @@ -1870,7 +1878,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } else if let gridSelectionGesture = self.gridSelectionGesture { self.itemGrid.view.removeGestureRecognizer(gridSelectionGesture) self.gridSelectionGesture = nil - }*/ + } } private func updateHiddenItems() { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift index 7b09f0d194..be2213e218 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift @@ -473,7 +473,7 @@ private final class GenericItemLayer: CALayer, ItemLayer { return !self.hasContents } - func update(size: CGSize) { + func update(size: CGSize, insets: UIEdgeInsets, displayItem: SparseItemGridDisplayItem, binding: SparseItemGridBinding, item: SparseItemGrid.Item?) { /*if let durationLayer = self.durationLayer { durationLayer.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 3.0), size: CGSize()) }*/ @@ -613,7 +613,7 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL return !self.hasContents } - func update(size: CGSize) { + func update(size: CGSize, insets: UIEdgeInsets, displayItem: SparseItemGridDisplayItem, binding: SparseItemGridBinding, item: SparseItemGrid.Item?) { /*if let durationLayer = self.durationLayer { durationLayer.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 3.0), size: CGSize()) }*/ diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index 9dcc82eda6..241d8be961 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -914,7 +914,9 @@ public final class StoryContentContextImpl: StoryContentContext { } public func markAsSeen(id: StoryId) { - let _ = self.context.engine.messages.markStoryAsSeen(peerId: id.peerId, id: id.id, asPinned: false).start() + if !self.context.sharedContext.immediateExperimentalUISettings.skipReadHistory { + let _ = self.context.engine.messages.markStoryAsSeen(peerId: id.peerId, id: id.id, asPinned: false).start() + } } } @@ -1094,7 +1096,9 @@ public final class SingleStoryContentContextImpl: StoryContentContext { public func markAsSeen(id: StoryId) { if self.readGlobally { - let _ = self.context.engine.messages.markStoryAsSeen(peerId: id.peerId, id: id.id, asPinned: false).start() + if !self.context.sharedContext.immediateExperimentalUISettings.skipReadHistory { + let _ = self.context.engine.messages.markStoryAsSeen(peerId: id.peerId, id: id.id, asPinned: false).start() + } } } } @@ -1397,7 +1401,9 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { } public func markAsSeen(id: StoryId) { - let _ = self.context.engine.messages.markStoryAsSeen(peerId: id.peerId, id: id.id, asPinned: true).start() + if !self.context.sharedContext.immediateExperimentalUISettings.skipReadHistory { + let _ = self.context.engine.messages.markStoryAsSeen(peerId: id.peerId, id: id.id, asPinned: true).start() + } } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 1fddf2e425..25ef1ec1bd 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -271,6 +271,7 @@ public final class StoryItemSetContainerComponent: Component { final class VisibleItem { let externalState = StoryContentItem.ExternalState() let contentContainerView: UIView + let contentTintLayer = SimpleLayer() let view = ComponentView() var currentProgress: Double = 0.0 var isBuffering: Bool = false @@ -282,6 +283,8 @@ public final class StoryItemSetContainerComponent: Component { if #available(iOS 13.0, *) { self.contentContainerView.layer.cornerCurve = .continuous } + + self.contentTintLayer.backgroundColor = UIColor(white: 0.0, alpha: 1.0).cgColor } } @@ -952,6 +955,9 @@ public final class StoryItemSetContainerComponent: Component { if self.sendMessageContext.shareController != nil { return .pause } + if self.sendMessageContext.statusController != nil { + return .pause + } if let navigationController = component.controller()?.navigationController as? NavigationController { let topViewController = navigationController.topViewController if !(topViewController is StoryContainerScreen) && !(topViewController is MediaEditorScreen) && !(topViewController is ShareWithPeersScreen) && !(topViewController is AttachmentController) { @@ -1119,6 +1125,7 @@ public final class StoryItemSetContainerComponent: Component { if let view = visibleItem.view.view { if visibleItem.contentContainerView.superview == nil { self.itemsContainerView.addSubview(visibleItem.contentContainerView) + self.itemsContainerView.layer.addSublayer(visibleItem.contentTintLayer) visibleItem.contentContainerView.addSubview(view) } @@ -1137,6 +1144,9 @@ public final class StoryItemSetContainerComponent: Component { }) itemTransition.setBounds(view: visibleItem.contentContainerView, bounds: CGRect(origin: CGPoint(), size: itemLayout.contentFrame.size)) + itemTransition.setPosition(layer: visibleItem.contentTintLayer, position: CGPoint(x: itemPositionX, y: itemLayout.contentFrame.center.y)) + itemTransition.setBounds(layer: visibleItem.contentTintLayer, bounds: CGRect(origin: CGPoint(), size: itemLayout.contentFrame.size)) + var transform = CATransform3DMakeScale(itemScale, itemScale, 1.0) if let pinchState = component.pinchState { let pinchOffset = CGPoint( @@ -1154,6 +1164,8 @@ public final class StoryItemSetContainerComponent: Component { itemTransition.setTransform(view: visibleItem.contentContainerView, transform: transform) itemTransition.setCornerRadius(layer: visibleItem.contentContainerView.layer, cornerRadius: 12.0 * (1.0 / itemScale)) + itemTransition.setTransform(layer: visibleItem.contentTintLayer, transform: transform) + let countedFractionDistanceToCenter: CGFloat = max(0.0, min(1.0, unboundFractionDistanceToCenter / 3.0)) var itemAlpha: CGFloat = 1.0 * (1.0 - countedFractionDistanceToCenter) + 0.0 * countedFractionDistanceToCenter itemAlpha = max(0.0, min(1.0, itemAlpha)) @@ -1161,7 +1173,7 @@ public final class StoryItemSetContainerComponent: Component { let collapsedAlpha = itemAlpha * itemLayout.contentScaleFraction + 0.0 * (1.0 - itemLayout.contentScaleFraction) itemAlpha = (1.0 - fractionDistanceToCenter) * itemAlpha + fractionDistanceToCenter * collapsedAlpha - itemTransition.setAlpha(view: visibleItem.contentContainerView, alpha: itemAlpha) + itemTransition.setAlpha(layer: visibleItem.contentTintLayer, alpha: 1.0 - itemAlpha) var itemProgressMode = self.itemProgressMode() if index != centralIndex { @@ -1182,6 +1194,7 @@ public final class StoryItemSetContainerComponent: Component { if !validIds.contains(id) { removeIds.append(id) visibleItem.contentContainerView.removeFromSuperview() + visibleItem.contentTintLayer.removeFromSuperlayer() } } for id in removeIds { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index cb3c04f565..3cbf85aa86 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -57,6 +57,7 @@ final class StoryItemSetContainerSendMessage { weak var shareController: ShareController? weak var tooltipScreen: ViewController? weak var actionSheet: ViewController? + weak var statusController: ViewController? var isViewingAttachedStickers = false var currentInputMode: InputMode = .text @@ -2473,14 +2474,21 @@ final class StoryItemSetContainerSendMessage { var cancelImpl: (() -> Void)? let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - let progressSignal = Signal { [weak parentController] subscriber in + let progressSignal = Signal { [weak self, weak view, weak parentController] subscriber in let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { cancelImpl?() })) parentController?.present(controller, in: .window(.root)) + + self?.statusController = controller + view?.updateIsProgressPaused() + return ActionDisposable { [weak controller] in Queue.mainQueue().async() { controller?.dismiss() + + self?.statusController = nil + view?.updateIsProgressPaused() } } } @@ -2547,14 +2555,21 @@ final class StoryItemSetContainerSendMessage { } var cancelImpl: (() -> Void)? let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - let progressSignal = Signal { [weak parentController] subscriber in + let progressSignal = Signal { [weak parentController, weak self, weak view] subscriber in let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { cancelImpl?() })) parentController?.present(controller, in: .window(.root)) + + self?.statusController = controller + view?.updateIsProgressPaused() + return ActionDisposable { [weak controller] in Queue.mainQueue().async() { controller?.dismiss() + + self?.statusController = nil + view?.updateIsProgressPaused() } } } @@ -2739,12 +2754,19 @@ final class StoryItemSetContainerSendMessage { } let context = component.context let presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) - let progressSignal = Signal { [weak parentController] subscriber in + let progressSignal = Signal { [weak parentController, weak self, weak view] subscriber in let progressController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) parentController?.present(progressController, in: .window(.root), with: nil) + + self?.statusController = progressController + view?.updateIsProgressPaused() + return ActionDisposable { [weak progressController] in Queue.mainQueue().async() { progressController?.dismiss() + + self?.statusController = nil + view?.updateIsProgressPaused() } } } diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index 2014e0089f..ecbfb1c92f 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -356,6 +356,8 @@ public final class StoryPeerListComponent: Component { public private(set) var overscrollSelectedId: EnginePeer.Id? public private(set) var overscrollHiddenChatItemsAllowed: Bool = false + private var anchorForTooltipRect: CGRect? + private var sharedBlurEffect: NSObject? public override init(frame: CGRect) { @@ -482,7 +484,11 @@ public final class StoryPeerListComponent: Component { } public func anchorForTooltip() -> (UIView, CGRect)? { - return (self.collapsedButton, self.collapsedButton.bounds) + if let anchorForTooltipRect = self.anchorForTooltipRect { + return (self, anchorForTooltipRect) + } else { + return nil + } } public func titleFrame() -> CGRect { @@ -631,6 +637,7 @@ public final class StoryPeerListComponent: Component { var minFraction: CGFloat var maxFraction: CGFloat var sideAlphaFraction: CGFloat + var expandEffectFraction: CGFloat var titleWidth: CGFloat var activityFraction: CGFloat } @@ -668,6 +675,18 @@ public final class StoryPeerListComponent: Component { let timestamp = CACurrentMediaTime() + let calculateOverscrollEffectFraction: (CGFloat, CGFloat) -> CGFloat = { maxFraction, bounceFraction in + var expandEffectFraction: CGFloat = max(0.0, min(1.0, maxFraction)) + expandEffectFraction = 1.0 - pow(1.0 - expandEffectFraction, 2.0) + + let overscrollEffectFraction = max(0.0, maxFraction - 1.0) + expandEffectFraction += overscrollEffectFraction * 0.1 + + expandEffectFraction += pow(bounceFraction, 1.4) * 0.2 * maxFraction + + return expandEffectFraction + } + let collapsedState: CollapseState let expandBoundsFraction: CGFloat if let animationState = self.animationState { @@ -703,16 +722,6 @@ public final class StoryPeerListComponent: Component { let animatedTitleWidth = animationState.interpolatedFraction(at: timestamp, effectiveFromFraction: animationState.fromTitleWidth, toFraction: realTitleContentWidth) let animatedActivityFraction = animationState.interpolatedFraction(at: timestamp, effectiveFromFraction: animationState.fromActivityFraction, toFraction: targetActivityFraction) - collapsedState = CollapseState( - globalFraction: animatedGlobalFraction, - scaleFraction: animatedScaleFraction, - minFraction: animatedMinFraction, - maxFraction: animatedMaxFraction, - sideAlphaFraction: animatedSideAlphaFraction, - titleWidth: animatedTitleWidth, - activityFraction: animatedActivityFraction - ) - var rawProgress = CGFloat((timestamp - animationState.startTime) / animationState.duration) rawProgress = max(0.0, min(1.0, rawProgress)) @@ -724,6 +733,17 @@ public final class StoryPeerListComponent: Component { } else { expandBoundsFraction = 0.0 } + + collapsedState = CollapseState( + globalFraction: animatedGlobalFraction, + scaleFraction: animatedScaleFraction, + minFraction: animatedMinFraction, + maxFraction: animatedMaxFraction, + sideAlphaFraction: animatedSideAlphaFraction, + expandEffectFraction: calculateOverscrollEffectFraction(animatedMaxFraction, expandBoundsFraction), + titleWidth: animatedTitleWidth, + activityFraction: animatedActivityFraction + ) } else { collapsedState = CollapseState( globalFraction: targetFraction, @@ -731,6 +751,7 @@ public final class StoryPeerListComponent: Component { minFraction: targetMinFraction, maxFraction: targetMaxFraction, sideAlphaFraction: targetSideAlphaFraction, + expandEffectFraction: calculateOverscrollEffectFraction(targetMaxFraction, 0.0), titleWidth: realTitleContentWidth, activityFraction: targetActivityFraction ) @@ -1031,6 +1052,7 @@ public final class StoryPeerListComponent: Component { scale: itemScale, fullWidth: expandedItemWidth, expandedAlphaFraction: collapsedState.sideAlphaFraction, + expandEffectFraction: collapsedState.expandEffectFraction, leftNeighborDistance: leftNeighborDistance, rightNeighborDistance: rightNeighborDistance, action: component.peerAction, @@ -1166,6 +1188,7 @@ public final class StoryPeerListComponent: Component { scale: itemScale, fullWidth: expandedItemWidth, expandedAlphaFraction: collapsedState.sideAlphaFraction, + expandEffectFraction: collapsedState.expandEffectFraction, leftNeighborDistance: leftNeighborDistance, rightNeighborDistance: rightNeighborDistance, action: component.peerAction, @@ -1229,6 +1252,7 @@ public final class StoryPeerListComponent: Component { } transition.setFrame(view: self.collapsedButton, frame: CGRect(origin: CGPoint(x: component.minTitleX, y: 6.0 - 59.0), size: CGSize(width: max(0.0, component.maxTitleX - component.minTitleX), height: 44.0))) + self.anchorForTooltipRect = CGRect(origin: CGPoint(x: collapsedContentOrigin, y: -59.0 + 6.0 + 2.0), size: CGSize(width: collapsedContentWidth, height: 44.0)) let defaultCollapsedTitleOffset: CGFloat = 0.0 diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift index 110968e8aa..1d0ed60dff 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift @@ -57,7 +57,7 @@ private func calculateCircleIntersection(center: CGPoint, otherCenter: CGPoint, return (point1Angle, point2Angle) } -private func calculateMergingCircleShape(center: CGPoint, leftCenter: CGPoint?, rightCenter: CGPoint?, radius: CGFloat, totalCount: Int, unseenCount: Int, isSeen: Bool, segmentFraction: CGFloat) -> CGPath { +private func calculateMergingCircleShape(center: CGPoint, leftCenter: CGPoint?, rightCenter: CGPoint?, radius: CGFloat, totalCount: Int, unseenCount: Int, isSeen: Bool, segmentFraction: CGFloat, rotationFraction: CGFloat) -> CGPath { let leftAngles = leftCenter.flatMap { calculateCircleIntersection(center: center, otherCenter: $0, radius: radius) } let rightAngles = rightCenter.flatMap { calculateCircleIntersection(center: center, otherCenter: $0, radius: radius) } @@ -113,7 +113,7 @@ private func calculateMergingCircleShape(center: CGPoint, leftCenter: CGPoint?, } var startAngle = segmentSpacingAngle * 0.5 - CGFloat.pi * 0.5 + CGFloat(i) * (segmentSpacingAngle + segmentAngle) - startAngle += (1.0 - segmentFraction) * CGFloat.pi * 2.0 * (-0.25) + startAngle += -1.0 * (1.0 - rotationFraction) * CGFloat.pi * 2.0 * 0.5 let endAngle = startAngle + segmentAngle path.move(to: CGPoint(x: center.x + cos(startAngle) * radius, y: center.y + sin(startAngle) * radius)) @@ -343,6 +343,7 @@ public final class StoryPeerListItemComponent: Component { public let scale: CGFloat public let fullWidth: CGFloat public let expandedAlphaFraction: CGFloat + public let expandEffectFraction: CGFloat public let leftNeighborDistance: CGPoint? public let rightNeighborDistance: CGPoint? public let action: (EnginePeer) -> Void @@ -361,6 +362,7 @@ public final class StoryPeerListItemComponent: Component { scale: CGFloat, fullWidth: CGFloat, expandedAlphaFraction: CGFloat, + expandEffectFraction: CGFloat, leftNeighborDistance: CGPoint?, rightNeighborDistance: CGPoint?, action: @escaping (EnginePeer) -> Void, @@ -378,6 +380,7 @@ public final class StoryPeerListItemComponent: Component { self.scale = scale self.fullWidth = fullWidth self.expandedAlphaFraction = expandedAlphaFraction + self.expandEffectFraction = expandEffectFraction self.leftNeighborDistance = leftNeighborDistance self.rightNeighborDistance = rightNeighborDistance self.action = action @@ -421,6 +424,9 @@ public final class StoryPeerListItemComponent: Component { if lhs.expandedAlphaFraction != rhs.expandedAlphaFraction { return false } + if lhs.expandEffectFraction != rhs.expandEffectFraction { + return false + } if lhs.leftNeighborDistance != rhs.leftNeighborDistance { return false } @@ -785,8 +791,8 @@ public final class StoryPeerListItemComponent: Component { } Transition.immediate.setShapeLayerPath(layer: self.avatarShapeLayer, path: avatarPath) - Transition.immediate.setShapeLayerPath(layer: self.indicatorShapeSeenLayer, path: calculateMergingCircleShape(center: indicatorCenter, leftCenter: mappedLeftCenter, rightCenter: mappedRightCenter, radius: indicatorRadius - indicatorLineUnseenWidth * 0.5, totalCount: component.totalCount, unseenCount: component.unseenCount, isSeen: true, segmentFraction: component.expandedAlphaFraction)) - Transition.immediate.setShapeLayerPath(layer: self.indicatorShapeUnseenLayer, path: calculateMergingCircleShape(center: indicatorCenter, leftCenter: mappedLeftCenter, rightCenter: mappedRightCenter, radius: indicatorRadius - indicatorLineUnseenWidth * 0.5, totalCount: component.totalCount, unseenCount: component.unseenCount, isSeen: false, segmentFraction: component.expandedAlphaFraction)) + Transition.immediate.setShapeLayerPath(layer: self.indicatorShapeSeenLayer, path: calculateMergingCircleShape(center: indicatorCenter, leftCenter: mappedLeftCenter, rightCenter: mappedRightCenter, radius: indicatorRadius - indicatorLineUnseenWidth * 0.5, totalCount: component.totalCount, unseenCount: component.unseenCount, isSeen: true, segmentFraction: component.expandedAlphaFraction, rotationFraction: component.expandEffectFraction)) + Transition.immediate.setShapeLayerPath(layer: self.indicatorShapeUnseenLayer, path: calculateMergingCircleShape(center: indicatorCenter, leftCenter: mappedLeftCenter, rightCenter: mappedRightCenter, radius: indicatorRadius - indicatorLineUnseenWidth * 0.5, totalCount: component.totalCount, unseenCount: component.unseenCount, isSeen: false, segmentFraction: component.expandedAlphaFraction, rotationFraction: component.expandEffectFraction)) let titleString: String if component.peer.id == component.context.account.peerId { diff --git a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift index 02e1c4b8a7..36aebbb207 100644 --- a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -338,7 +338,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { title = EnginePeer(peer).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) subtitle = nil } - actionTitle = "OPEN STORY" + actionTitle = item.presentationData.strings.Chat_OpenStory default: break }