diff --git a/MODULE.bazel b/MODULE.bazel index 3f8e4a31c5..a9aae97daf 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,6 +1,6 @@ http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file") -bazel_dep(name = "bazel_features", version = "1.29.0") +bazel_dep(name = "bazel_features", version = "1.30.0") bazel_dep(name = "bazel_skylib", version = "1.7.1") bazel_dep(name = "platforms", version = "0.0.11") diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index cf536336c5..ed86535e8b 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -20,9 +20,9 @@ "https://bcr.bazel.build/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a", "https://bcr.bazel.build/modules/bazel_features/1.19.0/MODULE.bazel": "59adcdf28230d220f0067b1f435b8537dd033bfff8db21335ef9217919c7fb58", "https://bcr.bazel.build/modules/bazel_features/1.21.0/MODULE.bazel": "675642261665d8eea09989aa3b8afb5c37627f1be178382c320d1b46afba5e3b", - "https://bcr.bazel.build/modules/bazel_features/1.29.0/MODULE.bazel": "fc2373c9825b71bcd06b60ef19b1715a42fa58edc4e39086375f47aa277e45ba", - "https://bcr.bazel.build/modules/bazel_features/1.29.0/source.json": "714dbd96075890f20f4f158ed9fa7fd1080bf6a6e0d5a89a05e2765e3ed74260", "https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9", + "https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87", + "https://bcr.bazel.build/modules/bazel_features/1.30.0/source.json": "b07e17f067fe4f69f90b03b36ef1e08fe0d1f3cac254c1241a1818773e3423bc", "https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7", "https://bcr.bazel.build/modules/bazel_features/1.9.0/MODULE.bazel": "885151d58d90d8d9c811eb75e3288c11f850e1d6b481a8c9f766adee4712358b", "https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a", @@ -81,12 +81,12 @@ "https://bcr.bazel.build/modules/rules_cc/0.0.14/MODULE.bazel": "5e343a3aac88b8d7af3b1b6d2093b55c347b8eefc2e7d1442f7a02dc8fea48ac", "https://bcr.bazel.build/modules/rules_cc/0.0.15/MODULE.bazel": "6704c35f7b4a72502ee81f61bf88706b54f06b3cbe5558ac17e2e14666cd5dcc", "https://bcr.bazel.build/modules/rules_cc/0.0.16/MODULE.bazel": "7661303b8fc1b4d7f532e54e9d6565771fea666fbdf839e0a86affcd02defe87", - "https://bcr.bazel.build/modules/rules_cc/0.0.17/MODULE.bazel": "2ae1d8f4238ec67d7185d8861cb0a2cdf4bc608697c331b95bf990e69b62e64a", - "https://bcr.bazel.build/modules/rules_cc/0.0.17/source.json": "4db99b3f55c90ab28d14552aa0632533e3e8e5e9aea0f5c24ac0014282c2a7c5", "https://bcr.bazel.build/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c", "https://bcr.bazel.build/modules/rules_cc/0.0.6/MODULE.bazel": "abf360251023dfe3efcef65ab9d56beefa8394d4176dd29529750e1c57eaa33f", "https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e", "https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5", + "https://bcr.bazel.build/modules/rules_cc/0.1.1/MODULE.bazel": "2f0222a6f229f0bf44cd711dc13c858dad98c62d52bd51d8fc3a764a83125513", + "https://bcr.bazel.build/modules/rules_cc/0.1.1/source.json": "d61627377bd7dd1da4652063e368d9366fc9a73920bfa396798ad92172cf645c", "https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6", "https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/MODULE.bazel": "40c97d1144356f52905566c55811f13b299453a14ac7769dfba2ac38192337a8", "https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/source.json": "c8b1e2c717646f1702290959a3302a178fb639d987ab61d548105019f11e527e", @@ -100,8 +100,8 @@ "https://bcr.bazel.build/modules/rules_java/7.2.0/MODULE.bazel": "06c0334c9be61e6cef2c8c84a7800cef502063269a5af25ceb100b192453d4ab", "https://bcr.bazel.build/modules/rules_java/7.3.2/MODULE.bazel": "50dece891cfdf1741ea230d001aa9c14398062f2b7c066470accace78e412bc2", "https://bcr.bazel.build/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe", - "https://bcr.bazel.build/modules/rules_java/8.11.0/MODULE.bazel": "c3d280bc5ff1038dcb3bacb95d3f6b83da8dd27bba57820ec89ea4085da767ad", - "https://bcr.bazel.build/modules/rules_java/8.11.0/source.json": "302b52a39259a85aa06ca3addb9787864ca3e03b432a5f964ea68244397e7544", + "https://bcr.bazel.build/modules/rules_java/8.12.0/MODULE.bazel": "8e6590b961f2defdfc2811c089c75716cb2f06c8a4edeb9a8d85eaa64ee2a761", + "https://bcr.bazel.build/modules/rules_java/8.12.0/source.json": "cbd5d55d9d38d4008a7d00bee5b5a5a4b6031fcd4a56515c9accbcd42c7be2ba", "https://bcr.bazel.build/modules/rules_java/8.3.2/MODULE.bazel": "7336d5511ad5af0b8615fdc7477535a2e4e723a357b6713af439fe8cf0195017", "https://bcr.bazel.build/modules/rules_java/8.5.1/MODULE.bazel": "d8a9e38cc5228881f7055a6079f6f7821a073df3744d441978e7a43e20226939", "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7", @@ -151,15 +151,15 @@ "https://bcr.bazel.build/modules/xcodeproj/8.27.3/MODULE.bazel": "49276599207dae3df1e4336c2067505323dfb0606b53ef63e144087d1226e0eb", "https://bcr.bazel.build/modules/xcodeproj/8.27.3/source.json": "bbbb718187dcbdfbb3a9a0ec7d49446cdf48c67657cafd79b5cf33aa8918f608", "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0", - "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/MODULE.bazel": "af322bc08976524477c79d1e45e241b6efbeb918c497e8840b8ab116802dda79", - "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/source.json": "2be409ac3c7601245958cd4fcdff4288be79ed23bd690b4b951f500d54ee6e7d", + "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca", + "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/source.json": "22bc55c47af97246cfc093d0acf683a7869377de362b5d1c552c2c2e16b7a806", "https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198" }, "selectedYankedVersions": {}, "moduleExtensions": { "@@apple_support+//crosstool:setup.bzl%apple_cc_configure_extension": { "general": { - "bzlTransitiveDigest": "IK7QnlhcNBu2jc4wZoGZeDTu3keF2LldFiFUINRcKvo=", + "bzlTransitiveDigest": "RjubjYIojbv0PxTpnoknalV9QzT9asbV7elDuN7m2A4=", "usagesDigest": "lfcV4HxPD+NLaRIT/v7BtSGFgE7c9xrWU7jDiwBAxzo=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -190,7 +190,7 @@ }, "@@rules_kotlin+//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": { "general": { - "bzlTransitiveDigest": "sFhcgPbDQehmbD1EOXzX4H1q/CD5df8zwG4kp4jbvr8=", + "bzlTransitiveDigest": "hUTp2w+RUVdL7ma5esCXZJAFnX7vLbVfLd7FwnQI6bU=", "usagesDigest": "QI2z8ZUR+mqtbwsf2fLqYdJAkPOHdOV+tF2yVAUgRzw=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -290,7 +290,7 @@ }, "@@rules_xcodeproj+//xcodeproj:extensions.bzl%internal": { "general": { - "bzlTransitiveDigest": "+kmqZtEKFY8zgqpV6mrwdQkTJqGUZhL8b3ZMsxrqSyc=", + "bzlTransitiveDigest": "6MYik+6MZUO7rOzaI0dUJYVD8dJrR1Q2rT+5vo1j73U=", "usagesDigest": "fvsnMonVwKDYnBfww4bXuYie3WU0d9VSqT2gePSdQco=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -312,7 +312,7 @@ }, "@@rules_xcodeproj+//xcodeproj:extensions.bzl%non_module_deps": { "general": { - "bzlTransitiveDigest": "+kmqZtEKFY8zgqpV6mrwdQkTJqGUZhL8b3ZMsxrqSyc=", + "bzlTransitiveDigest": "6MYik+6MZUO7rOzaI0dUJYVD8dJrR1Q2rT+5vo1j73U=", "usagesDigest": "jzxYhnOC9BE0dJ0biFLfxWXi/+R19uAAZkJ0p9CY0JI=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, diff --git a/build-system/Make/BazelLocation.py b/build-system/Make/BazelLocation.py index 7f27c970a8..fdc944ac77 100644 --- a/build-system/Make/BazelLocation.py +++ b/build-system/Make/BazelLocation.py @@ -91,8 +91,8 @@ def locate_bazel(base_path, cache_host_or_path, cache_dir): if test_sha256 == versions.bazel_version_sha256: shutil.copyfile(temp_output_file.name, bazel_path) elif resolved_cache_path is not None: - (cache_cas_id, cache_cas_name) = cache_cas_name(versions.bazel_version_sha256) - cached_path = '{}/cas/{}/{}'.format(resolved_cache_path, cache_cas_id, cache_cas_name) + (cache_cas_id, cache_cas_name_value) = cache_cas_name(versions.bazel_version_sha256) + cached_path = '{}/cas/{}/{}'.format(resolved_cache_path, cache_cas_id, cache_cas_name_value) if os.path.isfile(cached_path): shutil.copyfile(cached_path, bazel_path) @@ -137,8 +137,8 @@ def locate_bazel(base_path, cache_host_or_path, cache_dir): ) ], check_result=False) elif resolved_cache_path is not None: - (cache_cas_id, cache_cas_name) = cache_cas_name(versions.bazel_version_sha256) - cached_path = '{}/cas/{}/{}'.format(resolved_cache_path, cache_cas_id, cache_cas_name) + (cache_cas_id, cache_cas_name_value) = cache_cas_name(versions.bazel_version_sha256) + cached_path = '{}/cas/{}/{}'.format(resolved_cache_path, cache_cas_id, cache_cas_name_value) os.makedirs(os.path.dirname(cached_path), exist_ok=True) shutil.copyfile(bazel_path, cached_path) diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 1d3ab0132f..c44fadb222 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -330,6 +330,7 @@ public enum ResolvedUrl { case messageLink(link: TelegramResolvedMessageLink?) case stars case shareStory(Int64) + case storyFolder(peerId: PeerId, id: Int64) } public enum ResolveUrlResult { @@ -667,6 +668,7 @@ public enum PeerInfoControllerMode { case myProfileGifts case groupsInCommon case monoforum(EnginePeer.Id) + case storyAlbum(id: Int64) } public enum ContactListActionItemInlineIconPosition { diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift index 2da8cbbe44..1ddedfee3d 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift @@ -650,7 +650,8 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg } return self.controller?.navigationController as? NavigationController }, - listContext: storySearchContext + listContext: storySearchContext, + initialStoryFolderId: nil ) self.storySearchPaneNode = storySearchPaneNode if let storySearchView = self.storySearchComponentView?.view { diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 1a7854d0bf..a72c5b6fde 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -1462,7 +1462,7 @@ public func openPostbox(basePath: String, seedConfiguration: SeedConfiguration, #if DEBUG //debugSaveState(basePath: basePath + "/db", name: "previous2") - //debugRestoreState(basePath: basePath + "/db", name: "previous2") + debugRestoreState(basePath: basePath + "/db", name: "previous2") #endif let startTime = CFAbsoluteTimeGetCurrent() diff --git a/submodules/PremiumUI/Sources/CreateGiveawayController.swift b/submodules/PremiumUI/Sources/CreateGiveawayController.swift index cb1b3ce206..b7ebc06d50 100644 --- a/submodules/PremiumUI/Sources/CreateGiveawayController.swift +++ b/submodules/PremiumUI/Sources/CreateGiveawayController.swift @@ -1550,7 +1550,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio context: context, initialPrivacy: EngineStoryPrivacy(base: .nobody, additionallyIncludePeers: state.peers), stateContext: stateContext, - completion: { _, privacy ,_, _, _, _ in + completion: { _, privacy ,_, _, _, _, _ in updateState { state in var updatedState = state updatedState.peers = privacy.additionallyIncludePeers @@ -1589,7 +1589,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio context: context, initialPrivacy: EngineStoryPrivacy(base: .nobody, additionallyIncludePeers: state.peers), stateContext: stateContext, - completion: { _, privacy ,_, _, _, _ in + completion: { _, privacy ,_, _, _, _, _ in updateState { state in var updatedState = state updatedState.channels = privacy.additionallyIncludePeers diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index ad6c54879f..1ad554a126 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -5115,7 +5115,8 @@ func replayFinalState( isMy: item.isMy, myReaction: updatedReaction, forwardInfo: item.forwardInfo, - authorId: item.authorId + authorId: item.authorId, + folderIds: item.folderIds )) if let entry = CodableEntry(updatedItem) { updatedPeerEntries[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: item.expirationTimestamp, isCloseFriends: item.isCloseFriends) @@ -5149,7 +5150,8 @@ func replayFinalState( isMy: item.isMy, myReaction: MessageReaction.Reaction(apiReaction: reaction), forwardInfo: item.forwardInfo, - authorId: item.authorId + authorId: item.authorId, + folderIds: item.folderIds )) if let entry = CodableEntry(updatedItem) { transaction.setStory(id: StoryId(peerId: peerId, id: id), value: entry) diff --git a/submodules/TelegramCore/Sources/Statistics/StoryStatistics.swift b/submodules/TelegramCore/Sources/Statistics/StoryStatistics.swift index 29dd3fbfcb..e7f3b86f94 100644 --- a/submodules/TelegramCore/Sources/Statistics/StoryStatistics.swift +++ b/submodules/TelegramCore/Sources/Statistics/StoryStatistics.swift @@ -350,7 +350,8 @@ private final class StoryStatsPublicForwardsContextImpl { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }, - author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) } + author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) }, + folderIds: item.folderIds ) resultForwards.append(.story(EnginePeer(peer), mappedItem)) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift index efcd222dd5..ce2e689455 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift @@ -559,7 +559,8 @@ public final class EngineStoryViewListContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }, - author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) } + author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) }, + folderIds: item.folderIds ), storyStats: transaction.getPeerStoryStats(peerId: peer.id) ))) @@ -599,7 +600,8 @@ public final class EngineStoryViewListContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo, - authorId: item.authorId + authorId: item.authorId, + folderIds: item.folderIds )) if let entry = CodableEntry(updatedItem) { transaction.setStory(id: StoryId(peerId: account.peerId, id: storyId), value: entry) @@ -639,7 +641,8 @@ public final class EngineStoryViewListContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo, - authorId: item.authorId + authorId: item.authorId, + folderIds: item.folderIds )) if let entry = CodableEntry(updatedItem) { currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends) @@ -756,7 +759,8 @@ public final class EngineStoryViewListContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }, - author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) } + author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) }, + folderIds: item.folderIds ), storyStats: transaction.getPeerStoryStats(peerId: peer.id) ))) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift index 0fa08c6ef1..b350443e9b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift @@ -98,6 +98,7 @@ public extension Stories { case randomId case forwardInfo case uploadInfo + case folders } public let target: PendingTarget @@ -114,6 +115,7 @@ public extension Stories { public let period: Int32 public let randomId: Int64 public let forwardInfo: PendingForwardInfo? + public let folders: [Int64] public let uploadInfo: StoryUploadInfo? public init( @@ -131,6 +133,7 @@ public extension Stories { period: Int32, randomId: Int64, forwardInfo: PendingForwardInfo?, + folders: [Int64], uploadInfo: StoryUploadInfo? ) { self.target = target @@ -147,6 +150,7 @@ public extension Stories { self.period = period self.randomId = randomId self.forwardInfo = forwardInfo + self.folders = folders self.uploadInfo = uploadInfo } @@ -176,6 +180,8 @@ public extension Stories { self.forwardInfo = try container.decodeIfPresent(PendingForwardInfo.self, forKey: .forwardInfo) + self.folders = try container.decodeIfPresent([Int64].self, forKey: .folders) ?? [] + self.uploadInfo = try container.decodeIfPresent(StoryUploadInfo.self, forKey: .uploadInfo) } @@ -205,6 +211,7 @@ public extension Stories { try container.encode(self.period, forKey: .period) try container.encode(self.randomId, forKey: .randomId) try container.encodeIfPresent(self.forwardInfo, forKey: .forwardInfo) + try container.encode(self.folders, forKey: .folders) try container.encodeIfPresent(self.uploadInfo, forKey: .uploadInfo) } @@ -245,6 +252,9 @@ public extension Stories { if lhs.forwardInfo != rhs.forwardInfo { return false } + if lhs.folders != rhs.folders { + return false + } if lhs.uploadInfo != rhs.uploadInfo { return false } @@ -473,7 +483,7 @@ final class PendingStoryManager { let partTotalProgress = 1.0 / Float(uploadInfo.total) pendingItemContext.progress = Float(uploadInfo.index) * partTotalProgress } - pendingItemContext.disposable = (_internal_uploadStoryImpl(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, stateManager: self.stateManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, revalidationContext: self.revalidationContext, auxiliaryMethods: self.auxiliaryMethods, toPeerId: toPeerId, stableId: stableId, media: firstItem.media, mediaAreas: firstItem.mediaAreas, text: firstItem.text, entities: firstItem.entities, embeddedStickers: firstItem.embeddedStickers, pin: firstItem.pin, privacy: firstItem.privacy, isForwardingDisabled: firstItem.isForwardingDisabled, period: Int(firstItem.period), randomId: firstItem.randomId, forwardInfo: firstItem.forwardInfo) + pendingItemContext.disposable = (_internal_uploadStoryImpl(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, stateManager: self.stateManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, revalidationContext: self.revalidationContext, auxiliaryMethods: self.auxiliaryMethods, toPeerId: toPeerId, stableId: stableId, media: firstItem.media, mediaAreas: firstItem.mediaAreas, text: firstItem.text, entities: firstItem.entities, embeddedStickers: firstItem.embeddedStickers, pin: firstItem.pin, privacy: firstItem.privacy, isForwardingDisabled: firstItem.isForwardingDisabled, period: Int(firstItem.period), folders: firstItem.folders, randomId: firstItem.randomId, forwardInfo: firstItem.forwardInfo) |> deliverOn(self.queue)).start(next: { [weak self] event in guard let `self` = self else { return diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 9f04c8492a..568830731d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -263,6 +263,7 @@ public enum Stories { case myReaction case forwardInfo case authorId + case folderIds } public let id: Int32 @@ -287,6 +288,7 @@ public enum Stories { public let myReaction: MessageReaction.Reaction? public let forwardInfo: ForwardInfo? public let authorId: PeerId? + public let folderIds: [Int64]? public init( id: Int32, @@ -310,7 +312,8 @@ public enum Stories { isMy: Bool, myReaction: MessageReaction.Reaction?, forwardInfo: ForwardInfo?, - authorId: PeerId? + authorId: PeerId?, + folderIds: [Int64]? ) { self.id = id self.timestamp = timestamp @@ -334,6 +337,7 @@ public enum Stories { self.myReaction = myReaction self.forwardInfo = forwardInfo self.authorId = authorId + self.folderIds = folderIds } public init(from decoder: Decoder) throws { @@ -381,6 +385,7 @@ public enum Stories { self.myReaction = try container.decodeIfPresent(MessageReaction.Reaction.self, forKey: .myReaction) self.forwardInfo = try container.decodeIfPresent(ForwardInfo.self, forKey: .forwardInfo) self.authorId = try container.decodeIfPresent(Int64.self, forKey: .authorId).flatMap { PeerId($0) } + self.folderIds = try container.decodeIfPresent([Int64].self, forKey: .folderIds) } public func encode(to encoder: Encoder) throws { @@ -422,6 +427,7 @@ public enum Stories { try container.encodeIfPresent(self.myReaction, forKey: .myReaction) try container.encodeIfPresent(self.forwardInfo, forKey: .forwardInfo) try container.encodeIfPresent(self.authorId?.toInt64(), forKey: .authorId) + try container.encodeIfPresent(self.folderIds, forKey: .folderIds) } public static func ==(lhs: Item, rhs: Item) -> Bool { @@ -497,6 +503,9 @@ public enum Stories { if lhs.authorId != rhs.authorId { return false } + if lhs.folderIds != rhs.folderIds { + return false + } return true } } @@ -988,7 +997,7 @@ public struct StoryUploadInfo: Codable, Equatable { } } -func _internal_uploadStory(account: Account, target: Stories.PendingTarget, media: EngineStoryInputMedia, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, period: Int, randomId: Int64, forwardInfo: Stories.PendingForwardInfo?, uploadInfo: StoryUploadInfo? = nil) -> Signal { +func _internal_uploadStory(account: Account, target: Stories.PendingTarget, media: EngineStoryInputMedia, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, period: Int, randomId: Int64, forwardInfo: Stories.PendingForwardInfo?, folders: [Int64], uploadInfo: StoryUploadInfo? = nil) -> Signal { let inputMedia = prepareUploadStoryContent(account: account, media: media) return (account.postbox.transaction { transaction in @@ -1017,6 +1026,7 @@ func _internal_uploadStory(account: Account, target: Stories.PendingTarget, medi period: Int32(period), randomId: randomId, forwardInfo: forwardInfo, + folders: folders, uploadInfo: uploadInfo )) transaction.setLocalStoryState(state: CodableEntry(currentState)) @@ -1064,6 +1074,7 @@ func _internal_cancelStoryUpload(account: Account, stableId: Int32) { period: currentState.items[i].period, randomId: currentState.items[i].randomId, forwardInfo: currentState.items[i].forwardInfo, + folders: currentState.items[i].folders, uploadInfo: StoryUploadInfo( groupingId: groupingId, index: newIndex, @@ -1141,6 +1152,7 @@ func _internal_uploadStoryImpl( privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, period: Int, + folders: [Int64], randomId: Int64, forwardInfo: Stories.PendingForwardInfo? ) -> Signal { @@ -1224,6 +1236,10 @@ func _internal_uploadStoryImpl( fwdFromStory = forwardInfo.storyId } + if !folders.isEmpty { + flags |= 1 << 8 + } + return network.request(Api.functions.stories.sendStory( flags: flags, peer: inputPeer, @@ -1236,7 +1252,7 @@ func _internal_uploadStoryImpl( period: Int32(period), fwdFromId: fwdFromId, fwdFromStory: fwdFromStory, - albums: nil + albums: folders.isEmpty ? nil : folders.map(Int32.init(clamping:)) )) |> map(Optional.init) |> `catch` { _ -> Signal in @@ -1287,7 +1303,8 @@ func _internal_uploadStoryImpl( isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo, - authorId: fromId?.peerId + authorId: fromId?.peerId, + folderIds: item.folderIds ) if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { items.append(StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends)) @@ -1695,7 +1712,8 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo, - authorId: item.authorId + authorId: item.authorId, + folderIds: item.folderIds ) if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { transaction.setStory(id: storyId, value: entry) @@ -1727,7 +1745,8 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo, - authorId: item.authorId + authorId: item.authorId, + folderIds: item.folderIds ) if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends) @@ -1923,7 +1942,8 @@ func _internal_updateStoriesArePinned(account: Account, peerId: PeerId, ids: [In isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo, - authorId: item.authorId + authorId: item.authorId, + folderIds: item.folderIds ) if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends) @@ -1954,7 +1974,8 @@ func _internal_updateStoriesArePinned(account: Account, peerId: PeerId, ids: [In isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo, - authorId: item.authorId + authorId: item.authorId, + folderIds: item.folderIds ) updatedItems.append(updatedItem) } @@ -2073,7 +2094,12 @@ extension Stories.Item.ForwardInfo { extension Stories.StoredItem { init?(apiStoryItem: Api.StoryItem, existingItem: Stories.Item? = nil, peerId: PeerId, transaction: Transaction) { switch apiStoryItem { - case let .storyItem(flags, id, date, fromId, forwardFrom, expireDate, caption, entities, media, mediaAreas, privacy, views, sentReaction, _): + case let .storyItem(flags, id, date, fromId, forwardFrom, expireDate, caption, entities, media, mediaAreas, privacy, views, sentReaction, albums): + var folderIds: [Int64]? + if let albums { + folderIds = albums.map(Int64.init) + } + let parsedMedia = textMediaAndExpirationTimerFromApiMedia(media, peerId).media if let parsedMedia = parsedMedia { var parsedPrivacy: Stories.Item.Privacy? @@ -2185,7 +2211,8 @@ extension Stories.StoredItem { isMy: mergedIsMy, myReaction: mergedMyReaction, forwardInfo: mergedForwardInfo, - authorId: fromId?.peerId + authorId: fromId?.peerId, + folderIds: folderIds ) self = .item(item) } else { @@ -2262,7 +2289,8 @@ func _internal_getStoryById(accountPeerId: PeerId, postbox: Postbox, network: Ne isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }, - author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) } + author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) }, + folderIds: item.folderIds ) } } @@ -2739,7 +2767,8 @@ func _internal_setStoryReaction(account: Account, peerId: EnginePeer.Id, id: Int isMy: item.isMy, myReaction: reaction, forwardInfo: item.forwardInfo, - authorId: item.authorId + authorId: item.authorId, + folderIds: item.folderIds )) updatedItemValue = updatedItem if let entry = CodableEntry(updatedItem) { @@ -2773,7 +2802,8 @@ func _internal_setStoryReaction(account: Account, peerId: EnginePeer.Id, id: Int isMy: item.isMy, myReaction: reaction, forwardInfo: item.forwardInfo, - authorId: item.authorId + authorId: item.authorId, + folderIds: item.folderIds )) updatedItemValue = updatedItem if let entry = CodableEntry(updatedItem) { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index c0c957fe6d..1500524266 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -86,8 +86,9 @@ public final class EngineStoryItem: Equatable { public let myReaction: MessageReaction.Reaction? public let forwardInfo: ForwardInfo? public let author: EnginePeer? + public let folderIds: [Int64]? - public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, alternativeMediaList: [EngineMedia], mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool, isCloseFriends: Bool, isContacts: Bool, isSelectedContacts: Bool, isForwardingDisabled: Bool, isEdited: Bool, isMy: Bool, myReaction: MessageReaction.Reaction?, forwardInfo: ForwardInfo?, author: EnginePeer?) { + public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, alternativeMediaList: [EngineMedia], mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool, isCloseFriends: Bool, isContacts: Bool, isSelectedContacts: Bool, isForwardingDisabled: Bool, isEdited: Bool, isMy: Bool, myReaction: MessageReaction.Reaction?, forwardInfo: ForwardInfo?, author: EnginePeer?, folderIds: [Int64]?) { self.id = id self.timestamp = timestamp self.expirationTimestamp = expirationTimestamp @@ -111,6 +112,7 @@ public final class EngineStoryItem: Equatable { self.myReaction = myReaction self.forwardInfo = forwardInfo self.author = author + self.folderIds = folderIds } public static func ==(lhs: EngineStoryItem, rhs: EngineStoryItem) -> Bool { @@ -183,6 +185,9 @@ public final class EngineStoryItem: Equatable { if lhs.author != rhs.author { return false } + if lhs.folderIds != rhs.folderIds { + return false + } return true } } @@ -237,7 +242,8 @@ public extension EngineStoryItem { myReaction: self.myReaction, forwardInfo: self.forwardInfo?.storedForwardInfo, - authorId: self.author?.id + authorId: self.author?.id, + folderIds: self.folderIds ) } } @@ -679,6 +685,8 @@ public final class PeerStoryListContext: StoryListContext { return (nil, [], [], 0, [:], [], false) } + let mainFolders: [State.Folder] = cachedMain.folders + let cached: CachedPeerStoryListHead if let folderId { let key = ValueBoxKey(length: 8 + 1 + 8) @@ -733,7 +741,8 @@ public final class PeerStoryListContext: StoryListContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }, - author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) } + author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) }, + folderIds: item.folderIds ) items.append(State.Item( id: StoryId(peerId: peerId, id: mappedItem.id), @@ -768,7 +777,7 @@ public final class PeerStoryListContext: StoryListContext { let peerReference = transaction.getPeer(peerId).flatMap(PeerReference.init) - return (peerReference, items, cached.pinnedIds, Int(cached.totalCount), allEntityFiles, cached.folders, true) + return (peerReference, items, cached.pinnedIds, Int(cached.totalCount), allEntityFiles, mainFolders, true) } |> deliverOn(self.queue)).start(next: { [weak self] peerReference, items, pinnedIds, totalCount, allEntityFiles, folders, hasCache in guard let self else { @@ -828,103 +837,81 @@ public final class PeerStoryListContext: StoryListContext { let accountPeerId = account.peerId let isArchived = self.isArchived let folderId = self.folderId - let folders = self.stateValue.availableFolders + let queue = self.queue self.requestDisposable = (self.account.postbox.transaction { transaction -> Api.InputPeer? in return transaction.getPeer(peerId).flatMap(apiInputPeer) } - |> mapToSignal { inputPeer -> Signal<([State.Item], Int, PeerReference?, Bool), NoError> in - guard let inputPeer = inputPeer else { - return .single(([], 0, nil, false)) + |> mapToSignal { inputPeer -> Signal<([State.Item], Int, PeerReference?, [State.Folder], Bool), NoError> in + guard let inputPeer else { + return .single(([], 0, nil, [], false)) } - let signal: Signal - if isArchived { - signal = account.network.request(Api.functions.stories.getStoriesArchive(peer: inputPeer, offsetId: Int32(loadMoreToken), limit: Int32(limit))) + var signal: Signal + var additionalFolders: Signal = .single(nil) + if let folderId { + signal = account.network.request(Api.functions.stories.getAlbumStories(peer: inputPeer, albumId: Int32(clamping: folderId), offset: Int32(loadMoreToken), limit: Int32(limit))) |> map(Optional.init) } else { - signal = account.network.request(Api.functions.stories.getPinnedStories(peer: inputPeer, offsetId: Int32(loadMoreToken), limit: Int32(limit))) + if isArchived { + signal = account.network.request(Api.functions.stories.getStoriesArchive(peer: inputPeer, offsetId: Int32(loadMoreToken), limit: Int32(limit))) |> map(Optional.init) + } else { + signal = account.network.request(Api.functions.stories.getPinnedStories(peer: inputPeer, offsetId: Int32(loadMoreToken), limit: Int32(limit))) |> map(Optional.init) + additionalFolders = account.network.request(Api.functions.stories.getAlbums(peer: inputPeer, hash: 0)) + |> map(Optional.init) + } } - return signal - |> map { result -> Api.stories.Stories? in - return result + + #if DEBUG + if folderId != nil { + signal = signal |> delay(2.0, queue: queue) } - |> `catch` { _ -> Signal in - return .single(nil) - } - |> mapToSignal { result -> Signal<([State.Item], Int, PeerReference?, Bool), NoError> in - guard let result = result else { - return .single(([], 0, nil, false)) + #endif + + return combineLatest( + signal + |> `catch` { _ -> Signal in + return .single(nil) + }, + additionalFolders + |> `catch` { _ -> Signal in + return .single(nil) + } + ) + |> mapToSignal { result, updatedFolders -> Signal<([State.Item], Int, PeerReference?, [State.Folder], Bool), NoError> in + guard let result else { + return .single(([], 0, nil, [], false)) } - return account.postbox.transaction { transaction -> ([State.Item], Int, PeerReference?, Bool) in + return account.postbox.transaction { transaction -> ([State.Item], Int, PeerReference?, [State.Folder], Bool) in var storyItems: [State.Item] = [] var totalCount: Int = 0 var hasMore: Bool = false + var folderItems: [State.Folder] = [] - if let folderId { - let key = ValueBoxKey(length: 8 + 1 + 8) - key.setInt64(0, value: peerId.toInt64()) - key.setInt8(8, value: isArchived ? 1 : 0) - key.setInt64(8 + 1, value: folderId) - - var updatedItems: [Stories.StoredItem] = [] - if let currentEntry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key))?.get(CachedPeerStoryListHead.self) { - updatedItems = currentEntry.items - } - - if let entry = CodableEntry(CachedPeerStoryListHead(items: updatedItems, pinnedIds: [], totalCount: Int32(updatedItems.count), folders: [])) { - transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry) - } - - for item in updatedItems { - if case let .item(item) = item, let media = item.media { - let mappedItem = EngineStoryItem( - id: item.id, - timestamp: item.timestamp, - expirationTimestamp: item.expirationTimestamp, - media: EngineMedia(media), - alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init), - mediaAreas: item.mediaAreas, - text: item.text, - entities: item.entities, - views: item.views.flatMap { views in - return EngineStoryItem.Views( - seenCount: views.seenCount, - reactedCount: views.reactedCount, - forwardCount: views.forwardCount, - seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in - return transaction.getPeer(id).flatMap(EnginePeer.init) - }, - reactions: views.reactions, - hasList: views.hasList - ) - }, - privacy: item.privacy.flatMap(EngineStoryPrivacy.init), - isPinned: item.isPinned, - isExpired: item.isExpired, - isPublic: item.isPublic, - isPending: false, - isCloseFriends: item.isCloseFriends, - isContacts: item.isContacts, - isSelectedContacts: item.isSelectedContacts, - isForwardingDisabled: item.isForwardingDisabled, - isEdited: item.isEdited, - isMy: item.isMy, - myReaction: item.myReaction, - forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }, - author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) } - ) - storyItems.append(State.Item( - id: StoryId(peerId: peerId, id: mappedItem.id), - storyItem: mappedItem, - peer: nil - )) + if let updatedFolders { + switch updatedFolders { + case let .albums(_, albums): + for album in albums { + switch album { + case let .storyAlbum(_, albumId, title, iconPhoto, iconVideo): + let _ = iconPhoto + let _ = iconVideo + folderItems.append(State.Folder( + id: Int64(albumId), + title: title + )) + } } + case .albumsNotModified: + break } + } else { + let key = ValueBoxKey(length: 8 + 1) + key.setInt64(0, value: peerId.toInt64()) + key.setInt8(8, value: 0) - totalCount = storyItems.count - hasMore = false - - return (storyItems, totalCount, transaction.getPeer(peerId).flatMap(PeerReference.init), hasMore) + if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key))?.get(CachedPeerStoryListHead.self) { + folderItems = cached.folders + } } switch result { @@ -973,7 +960,8 @@ public final class PeerStoryListContext: StoryListContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }, - author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) } + author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) }, + folderIds: item.folderIds ) storyItems.append(State.Item( id: StoryId(peerId: peerId, id: mappedItem.id), @@ -985,20 +973,38 @@ public final class PeerStoryListContext: StoryListContext { } if loadMoreToken == 0 { + let key: ValueBoxKey + if let folderId { + key = ValueBoxKey(length: 8 + 1 + 8) + key.setInt64(0, value: peerId.toInt64()) + key.setInt8(8, value: isArchived ? 1 : 0) + key.setInt64(8 + 1, value: folderId) + } else { + key = ValueBoxKey(length: 8 + 1) + key.setInt64(0, value: peerId.toInt64()) + key.setInt8(8, value: isArchived ? 1 : 0) + } + if let entry = CodableEntry(CachedPeerStoryListHead(items: storyItems.prefix(100).map { .item($0.storyItem.asStoryItem()) }, pinnedIds: pinnedIds, totalCount: count, folders: folderId == nil ? folderItems : [])) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry) + } + } else if folderId == nil && updatedFolders != nil { let key = ValueBoxKey(length: 8 + 1) key.setInt64(0, value: peerId.toInt64()) - key.setInt8(8, value: isArchived ? 1 : 0) - if let entry = CodableEntry(CachedPeerStoryListHead(items: storyItems.prefix(100).map { .item($0.storyItem.asStoryItem()) }, pinnedIds: pinnedIds, totalCount: count, folders: folders)) { - transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry) + key.setInt8(8, value: 0) + + if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key))?.get(CachedPeerStoryListHead.self) { + if let entry = CodableEntry(CachedPeerStoryListHead(items: cached.items, pinnedIds: cached.pinnedIds, totalCount: cached.totalCount, folders: folderItems)) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry) + } } } } - return (storyItems, totalCount, transaction.getPeer(peerId).flatMap(PeerReference.init), hasMore) + return (storyItems, totalCount, transaction.getPeer(peerId).flatMap(PeerReference.init), folderItems, hasMore) } } } - |> deliverOn(self.queue)).start(next: { [weak self] storyItems, totalCount, peerReference, hasMore in + |> deliverOn(queue)).start(next: { [weak self] storyItems, totalCount, peerReference, folderItems, hasMore in guard let self else { return } @@ -1022,6 +1028,8 @@ public final class PeerStoryListContext: StoryListContext { updatedState.items.append(item) } + updatedState.availableFolders = folderItems + if updatedState.peerReference == nil { updatedState.peerReference = peerReference } @@ -1147,7 +1155,8 @@ public final class PeerStoryListContext: StoryListContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, peers: peers) }, - author: item.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) } + author: item.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) }, + folderIds: item.folderIds ), peer: nil) finalUpdatedState = updatedState } @@ -1196,7 +1205,8 @@ public final class PeerStoryListContext: StoryListContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, peers: peers) }, - author: item.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) } + author: item.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) }, + folderIds: item.folderIds ), peer: nil) finalUpdatedState = updatedState } else { @@ -1247,7 +1257,8 @@ public final class PeerStoryListContext: StoryListContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, peers: peers) }, - author: item.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) } + author: item.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) }, + folderIds: item.folderIds ), peer: nil)) updatedState.items.sort(by: { lhs, rhs in let lhsPinned = updatedState.pinnedIds.firstIndex(of: lhs.storyItem.id) @@ -1304,7 +1315,8 @@ public final class PeerStoryListContext: StoryListContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, peers: peers) }, - author: item.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) } + author: item.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) }, + folderIds: item.folderIds ), peer: nil)) updatedState.items.sort(by: { lhs, rhs in let lhsPinned = updatedState.pinnedIds.firstIndex(of: lhs.storyItem.id) @@ -1376,55 +1388,143 @@ public final class PeerStoryListContext: StoryListContext { }) } - func addFolder(title: String) -> Int64 { - let id = Int64.random(in: Int64.min ... Int64.max) - - var state = self.stateValue - state.availableFolders.append(StoryListContextState.Folder(id: id, title: title)) - self.stateValue = state - + func addFolder(title: String, items: [EngineStoryItem], completion: @escaping (Int64?) -> Void) -> Disposable { let peerId = self.peerId - let isArchived = self.isArchived - let items = state.items - let pinnedIds = state.pinnedIds - let totalCount = state.totalCount - let folders = state.availableFolders - let _ = (self.account.postbox.transaction { transaction -> Void in - let key = ValueBoxKey(length: 8 + 1) - key.setInt64(0, value: peerId.toInt64()) - key.setInt8(8, value: isArchived ? 1 : 0) - if let entry = CodableEntry(CachedPeerStoryListHead(items: items.prefix(100).map { .item($0.storyItem.asStoryItem()) }, pinnedIds: pinnedIds, totalCount: Int32(totalCount), folders: folders)) { - transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry) + return (self.account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) + } + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer else { + return .single(nil) } - }).start() - - return id + return self.account.network.request(Api.functions.stories.createAlbum( + peer: inputPeer, title: title, stories: items.map(\.id))) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + } + |> deliverOn(self.queue)).start(next: { [weak self] result in + guard let self else { + return + } + if let result { + switch result { + case let .storyAlbum(_, albumId, _, _, _): + var state = self.stateValue + state.availableFolders.insert(StoryListContextState.Folder(id: Int64(albumId), title: title), at: 0) + self.stateValue = state + + let peerId = self.peerId + let isArchived = self.isArchived + let items = state.items + let pinnedIds = state.pinnedIds + let totalCount = state.totalCount + let folders = state.availableFolders + let _ = (self.account.postbox.transaction { transaction -> Void in + let key = ValueBoxKey(length: 8 + 1) + key.setInt64(0, value: peerId.toInt64()) + key.setInt8(8, value: isArchived ? 1 : 0) + if let entry = CodableEntry(CachedPeerStoryListHead(items: items.prefix(100).map { .item($0.storyItem.asStoryItem()) }, pinnedIds: pinnedIds, totalCount: Int32(totalCount), folders: folders)) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry) + } + + let mappedItems = items.map { item in + return item.storyItem.asStoryItem() + } + + let folderKey = ValueBoxKey(length: 8 + 1 + 8) + folderKey.setInt64(0, value: peerId.toInt64()) + folderKey.setInt8(8, value: 0) + folderKey.setInt64(8 + 1, value: Int64(albumId)) + if let entry = CodableEntry(CachedPeerStoryListHead(items: mappedItems.prefix(100).map { .item($0) }, pinnedIds: [], totalCount: Int32(mappedItems.count), folders: [])) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: folderKey), entry: entry) + } + } |> deliverOn(self.queue)).start(completed: { + completion(Int64(albumId)) + }) + } + } else { + completion(nil) + } + }) } - func removeFolder(id: Int64) { + func removeFolder(id: Int64) -> Disposable { var state = self.stateValue if let index = state.availableFolders.firstIndex(where: { $0.id == id }) { state.availableFolders.remove(at: index) } self.stateValue = state + let account = self.account let peerId = self.peerId let isArchived = self.isArchived let items = state.items let pinnedIds = state.pinnedIds let totalCount = state.totalCount let folders = state.availableFolders - let _ = (self.account.postbox.transaction { transaction -> Void in + return (account.postbox.transaction { transaction -> Api.InputPeer? in let key = ValueBoxKey(length: 8 + 1) key.setInt64(0, value: peerId.toInt64()) key.setInt8(8, value: isArchived ? 1 : 0) if let entry = CodableEntry(CachedPeerStoryListHead(items: items.prefix(100).map { .item($0.storyItem.asStoryItem()) }, pinnedIds: pinnedIds, totalCount: Int32(totalCount), folders: folders)) { transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry) } + + return transaction.getPeer(peerId).flatMap(apiInputPeer) + } + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer else { + return .complete() + } + return account.network.request(Api.functions.stories.deleteAlbum(peer: inputPeer, albumId: Int32(clamping: id))) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> ignoreValues }).start() } - func reorderFolders(ids: [Int64]) { + func renameFolder(id: Int64, title: String) -> Disposable { + var state = self.stateValue + if let index = state.availableFolders.firstIndex(where: { $0.id == id }) { + state.availableFolders[index] = State.Folder(id: state.availableFolders[index].id, title: title) + } + self.stateValue = state + let updatedFolders = state.availableFolders + + let account = self.account + let peerId = self.peerId + return (account.postbox.transaction { transaction -> Api.InputPeer? in + let key = ValueBoxKey(length: 8 + 1) + key.setInt64(0, value: peerId.toInt64()) + key.setInt8(8, value: 0) + + if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key))?.get(CachedPeerStoryListHead.self) { + if let entry = CodableEntry(CachedPeerStoryListHead(items: cached.items, pinnedIds: cached.pinnedIds, totalCount: cached.totalCount, folders: updatedFolders)) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry) + } + } + + return transaction.getPeer(peerId).flatMap(apiInputPeer) + } + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer else { + return .complete() + } + var flags: Int32 = 0 + flags |= 1 << 0 + return account.network.request(Api.functions.stories.updateAlbum(flags: flags, peer: inputPeer, albumId: Int32(clamping: id), title: title, deleteStories: nil, addStories: nil, order: nil)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> ignoreValues + }).start() + } + + func reorderFolders(ids: [Int64]) -> Disposable { var state = self.stateValue var folders: [State.Folder] = [] for id in ids { @@ -1440,24 +1540,51 @@ public final class PeerStoryListContext: StoryListContext { state.availableFolders = folders self.stateValue = state + let account = self.account let peerId = self.peerId let isArchived = self.isArchived let items = state.items let pinnedIds = state.pinnedIds let totalCount = state.totalCount - let _ = (self.account.postbox.transaction { transaction -> Void in + return (account.postbox.transaction { transaction -> Api.InputPeer? in let key = ValueBoxKey(length: 8 + 1) key.setInt64(0, value: peerId.toInt64()) key.setInt8(8, value: isArchived ? 1 : 0) if let entry = CodableEntry(CachedPeerStoryListHead(items: items.prefix(100).map { .item($0.storyItem.asStoryItem()) }, pinnedIds: pinnedIds, totalCount: Int32(totalCount), folders: folders)) { transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry) } + return transaction.getPeer(peerId).flatMap(apiInputPeer) + } + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer else { + return .complete() + } + return account.network.request(Api.functions.stories.reorderAlbums(peer: inputPeer, order: ids.map(Int32.init(clamping:)))) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> ignoreValues }).start() } - func addToFolder(id: Int64, items: [EngineStoryItem]) { + func addToFolder(id: Int64, items: [EngineStoryItem]) -> Disposable { + if self.folderId == id { + var state = self.stateValue + for item in items { + state.items.removeAll(where: { $0.id.id == item.id }) + state.items.insert(StoryListContextState.Item( + id: StoryId(peerId: self.peerId, id: item.id), + storyItem: item, + peer: nil + ), at: 0) + } + state.totalCount = state.items.count + self.stateValue = state + } + + let account = self.account let peerId = self.peerId - let _ = (self.account.postbox.transaction { transaction -> Void in + return (account.postbox.transaction { transaction -> Api.InputPeer? in let key = ValueBoxKey(length: 8 + 1 + 8) key.setInt64(0, value: peerId.toInt64()) key.setInt8(8, value: 0) @@ -1518,7 +1645,8 @@ public final class PeerStoryListContext: StoryListContext { return .unknown(name: name, isModified: isModified) } }, - authorId: item.author?.id + authorId: item.author?.id, + folderIds: item.folderIds ) if !updatedItems.contains(where: { $0.id == mappedItem.id }) { updatedItems.insert(mappedItem, at: 0) @@ -1528,26 +1656,35 @@ public final class PeerStoryListContext: StoryListContext { if let entry = CodableEntry(CachedPeerStoryListHead(items: updatedItems.map { .item($0) }, pinnedIds: [], totalCount: Int32(updatedItems.count), folders: [])) { transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry) } + + return transaction.getPeer(peerId).flatMap(apiInputPeer) + } + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer else { + return .complete() + } + var flags: Int32 = 0 + flags |= 1 << 2 + return account.network.request(Api.functions.stories.updateAlbum(flags: flags, peer: inputPeer, albumId: Int32(clamping: id), title: nil, deleteStories: nil, addStories: items.map(\.id), order: nil)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> ignoreValues }).start() - + } + + func removeFromFolder(id: Int64, itemIds: [Int32]) -> Disposable { if self.folderId == id { var state = self.stateValue - for item in items { - state.items.removeAll(where: { $0.id.id == item.id }) - state.items.insert(StoryListContextState.Item( - id: StoryId(peerId: self.peerId, id: item.id), - storyItem: item, - peer: nil - ), at: 0) - } + state.items.removeAll(where: { itemIds.contains($0.id.id) }) state.totalCount = state.items.count self.stateValue = state } - } - - func removeFromFolder(id: Int64, itemIds: [Int32]) { + + let account = self.account let peerId = self.peerId - let _ = (self.account.postbox.transaction { transaction -> Void in + return (account.postbox.transaction { transaction -> Api.InputPeer? in let key = ValueBoxKey(length: 8 + 1 + 8) key.setInt64(0, value: peerId.toInt64()) key.setInt8(8, value: 0) @@ -1573,23 +1710,52 @@ public final class PeerStoryListContext: StoryListContext { if let entry = CodableEntry(CachedPeerStoryListHead(items: updatedItems.map { .item($0) }, pinnedIds: [], totalCount: Int32(updatedItems.count), folders: [])) { transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry) } + + return transaction.getPeer(peerId).flatMap(apiInputPeer) + } + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer else { + return .complete() + } + var flags: Int32 = 0 + flags |= 1 << 1 + return account.network.request(Api.functions.stories.updateAlbum(flags: flags, peer: inputPeer, albumId: Int32(clamping: id), title: nil, deleteStories: itemIds, addStories: nil, order: nil)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> ignoreValues }).start() + } + + func reorderItemsInFolder(itemIds: [Int32]) -> Disposable { + guard let id = self.folderId else { + return EmptyDisposable + } if self.folderId == id { var state = self.stateValue - state.items.removeAll(where: { itemIds.contains($0.id.id) }) - state.totalCount = state.items.count + + let previousItems = state.items + var updatedItems: [State.Item] = [] + for itemId in itemIds { + if let index = previousItems.firstIndex(where: { $0.id.id == itemId }) { + updatedItems.append(previousItems[index]) + } + } + for item in previousItems { + if !updatedItems.contains(where: { $0.id == item.id }) { + updatedItems.append(item) + } + } + + state.items = updatedItems self.stateValue = state } - } - - func reorderItemsInFolder(itemIds: [Int32]) { - guard let id = self.folderId else { - return - } + let account = self.account let peerId = self.peerId - let _ = (self.account.postbox.transaction { transaction -> Void in + return (account.postbox.transaction { transaction -> Api.InputPeer? in let key = ValueBoxKey(length: 8 + 1 + 8) key.setInt64(0, value: peerId.toInt64()) key.setInt8(8, value: 0) @@ -1622,27 +1788,22 @@ public final class PeerStoryListContext: StoryListContext { if let entry = CodableEntry(CachedPeerStoryListHead(items: updatedItems.map { .item($0) }, pinnedIds: [], totalCount: Int32(updatedItems.count), folders: [])) { transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry) } - }).start() - - if self.folderId == id { - var state = self.stateValue - let previousItems = state.items - var updatedItems: [State.Item] = [] - for itemId in itemIds { - if let index = previousItems.firstIndex(where: { $0.id.id == itemId }) { - updatedItems.append(previousItems[index]) - } - } - for item in previousItems { - if !updatedItems.contains(where: { $0.id == item.id }) { - updatedItems.append(item) - } - } - - state.items = updatedItems - self.stateValue = state + return transaction.getPeer(peerId).flatMap(apiInputPeer) } + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer else { + return .complete() + } + var flags: Int32 = 0 + flags |= 1 << 3 + return account.network.request(Api.functions.stories.updateAlbum(flags: flags, peer: inputPeer, albumId: Int32(clamping: id), title: nil, deleteStories: nil, addStories: nil, order: itemIds)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> ignoreValues + }).start() } } @@ -1676,61 +1837,100 @@ public final class PeerStoryListContext: StoryListContext { } } - public func addFolder(title: String, completion: @escaping (Int64) -> Void) { + public func addFolder(title: String, items: [EngineStoryItem], completion: @escaping (Int64?) -> Void) -> Disposable { + let disposable = MetaDisposable() self.impl.with { impl in - completion(impl.addFolder(title: title)) + disposable.set(impl.addFolder(title: title, items: items, completion: completion)) } + return disposable } - public func removeFolder(id: Int64) { + public func removeFolder(id: Int64) -> Disposable { + let disposable = MetaDisposable() self.impl.with { impl in - impl.removeFolder(id: id) + disposable.set(impl.removeFolder(id: id)) } + return disposable } - public func reorderFolders(ids: [Int64]) { + public func renameFolder(id: Int64, title: String) -> Disposable { + let disposable = MetaDisposable() self.impl.with { impl in - impl.reorderFolders(ids: ids) + disposable.set(impl.renameFolder(id: id, title: title)) } + return disposable } - public func addToFolder(id: Int64, items: [EngineStoryItem]) { + public func reorderFolders(ids: [Int64]) -> Disposable { + let disposable = MetaDisposable() self.impl.with { impl in - impl.addToFolder(id: id, items: items) + disposable.set(impl.reorderFolders(ids: ids)) } + return disposable } - public func removeFromFolder(id: Int64, itemIds: [Int32]) { + public func addToFolder(id: Int64, items: [EngineStoryItem]) -> Disposable { + let disposable = MetaDisposable() self.impl.with { impl in - impl.removeFromFolder(id: id, itemIds: itemIds) + disposable.set(impl.addToFolder(id: id, items: items)) } + return disposable } - public func reorderItemsInFolder(itemIds: [Int32]) { + public func removeFromFolder(id: Int64, itemIds: [Int32]) -> Disposable { + let disposable = MetaDisposable() self.impl.with { impl in - impl.reorderItemsInFolder(itemIds: itemIds) + disposable.set(impl.removeFromFolder(id: id, itemIds: itemIds)) } + return disposable } - public static func addFolderExternal(account: Account, peerId: PeerId, title: String) -> Int64 { - let id = Int64.random(in: Int64.min ... Int64.max) - - let _ = (account.postbox.transaction { transaction -> Void in - let key = ValueBoxKey(length: 8 + 1) - key.setInt64(0, value: peerId.toInt64()) - key.setInt8(8, value: 0) - - - let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key))?.get(CachedPeerStoryListHead.self) - var folders = cached?.folders ?? [] - folders.append(StoryListContextState.Folder(id: id, title: title)) - - if let entry = CodableEntry(CachedPeerStoryListHead(items: cached?.items ?? [], pinnedIds: cached?.pinnedIds ?? [], totalCount: cached?.totalCount ?? 0, folders: folders)) { - transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry) + public func reorderItemsInFolder(itemIds: [Int32]) -> Disposable { + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.reorderItemsInFolder(itemIds: itemIds)) + } + return disposable + } + + public static func addFolderExternal(account: Account, peerId: PeerId, title: String, completion: @escaping (Int64?) -> Void) -> Disposable { + return (account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) + } + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer else { + return .single(nil) } - }).start() - - return id + return account.network.request(Api.functions.stories.createAlbum( + peer: inputPeer, title: title, stories: [])) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + }).start(next: { result in + if let result { + switch result { + case let .storyAlbum(_, albumId, _, _, _): + let _ = (account.postbox.transaction { transaction -> Void in + let key = ValueBoxKey(length: 8 + 1) + key.setInt64(0, value: peerId.toInt64()) + key.setInt8(8, value: 0) + + let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key))?.get(CachedPeerStoryListHead.self) + var folders = cached?.folders ?? [] + folders.append(StoryListContextState.Folder(id: Int64(albumId), title: title)) + + if let entry = CodableEntry(CachedPeerStoryListHead(items: cached?.items ?? [], pinnedIds: cached?.pinnedIds ?? [], totalCount: cached?.totalCount ?? 0, folders: folders)) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry) + } + }).start(completed: { + completion(Int64(albumId)) + }) + } + } else { + completion(nil) + } + }) } public static func folderPreviews(peerId: EnginePeer.Id, account: Account) -> Signal<(peer: PeerReference, [(folder: StoryListContext.State.Folder, item: EngineStoryItem?)]), NoError> { @@ -1789,7 +1989,8 @@ public final class PeerStoryListContext: StoryListContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }, - author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) } + author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) }, + folderIds: item.folderIds ) } } @@ -1971,7 +2172,8 @@ public final class SearchStoryListContext: StoryListContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }, - author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) } + author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) }, + folderIds: item.folderIds ) storyItems.append(State.Item( id: StoryId(peerId: peer.peerId, id: mappedItem.id), @@ -1988,7 +2190,7 @@ public final class SearchStoryListContext: StoryListContext { } } |> deliverOn(self.queue)).start(next: { [weak self] storyItems, totalCount, nextOffset in - guard let `self` = self else { + guard let self else { return } @@ -2120,7 +2322,8 @@ public final class SearchStoryListContext: StoryListContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, peers: peers) }, - author: item.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) } + author: item.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) }, + folderIds: item.folderIds ), peer: currentItem.peer ) @@ -2181,7 +2384,8 @@ public final class SearchStoryListContext: StoryListContext { isMy: item.storyItem.isMy, myReaction: reaction, forwardInfo: item.storyItem.forwardInfo, - author: item.storyItem.author + author: item.storyItem.author, + folderIds: item.storyItem.folderIds ), peer: item.peer ) @@ -2310,7 +2514,8 @@ public final class PeerExpiringStoryListContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }, - author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) } + author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) }, + folderIds: item.folderIds ) items.append(.item(mappedItem)) } @@ -2756,7 +2961,8 @@ public final class BotPreviewStoryListContext: StoryListContext { isMy: false, myReaction: nil, forwardInfo: nil, - author: nil + author: nil, + folderIds: nil ), peer: nil )) @@ -2805,7 +3011,8 @@ public final class BotPreviewStoryListContext: StoryListContext { isMy: false, myReaction: nil, forwardInfo: nil, - author: nil + author: nil, + folderIds: nil ), peer: nil )) @@ -2917,7 +3124,8 @@ public final class BotPreviewStoryListContext: StoryListContext { isMy: false, myReaction: nil, forwardInfo: nil, - author: nil + author: nil, + folderIds: nil ), peer: nil )) @@ -2994,7 +3202,8 @@ public final class BotPreviewStoryListContext: StoryListContext { isMy: false, myReaction: nil, forwardInfo: nil, - author: nil + author: nil, + folderIds: nil ), peer: nil )) @@ -3057,7 +3266,8 @@ public final class BotPreviewStoryListContext: StoryListContext { isMy: false, myReaction: nil, forwardInfo: nil, - author: nil + author: nil, + folderIds: nil ), peer: nil ) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index ae90822692..707f091404 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -1353,7 +1353,8 @@ public extension TelegramEngine { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo, - authorId: item.authorId + authorId: item.authorId, + folderIds: item.folderIds )) if let entry = CodableEntry(updatedItem) { currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends) @@ -1367,8 +1368,8 @@ public extension TelegramEngine { } } - public func uploadStory(target: Stories.PendingTarget, media: EngineStoryInputMedia, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, period: Int, randomId: Int64, forwardInfo: Stories.PendingForwardInfo?, uploadInfo: StoryUploadInfo? = nil) -> Signal { - return _internal_uploadStory(account: self.account, target: target, media: media, mediaAreas: mediaAreas, text: text, entities: entities, pin: pin, privacy: privacy, isForwardingDisabled: isForwardingDisabled, period: period, randomId: randomId, forwardInfo: forwardInfo, uploadInfo: uploadInfo) + public func uploadStory(target: Stories.PendingTarget, media: EngineStoryInputMedia, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, period: Int, randomId: Int64, forwardInfo: Stories.PendingForwardInfo?, folders: [Int64], uploadInfo: StoryUploadInfo? = nil) -> Signal { + return _internal_uploadStory(account: self.account, target: target, media: media, mediaAreas: mediaAreas, text: text, entities: entities, pin: pin, privacy: privacy, isForwardingDisabled: isForwardingDisabled, period: period, randomId: randomId, forwardInfo: forwardInfo, folders: folders, uploadInfo: uploadInfo) } public func allStoriesUploadEvents() -> Signal<(Int32, Int32), NoError> { diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index 18f45863d7..fb24a05de9 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -1456,6 +1456,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { break case .shareStory: break + case .storyFolder: + break } } })) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift index 72690c911d..fbb97794e7 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift @@ -15,6 +15,7 @@ public struct MediaEditorResultPrivacy: Codable, Equatable { case timeout case disableForwarding case archive + case folderIds } public let sendAsPeerId: EnginePeer.Id? @@ -22,19 +23,22 @@ public struct MediaEditorResultPrivacy: Codable, Equatable { public let timeout: Int public let isForwardingDisabled: Bool public let pin: Bool + public let folderIds: [Int64] public init( sendAsPeerId: EnginePeer.Id?, privacy: EngineStoryPrivacy, timeout: Int, isForwardingDisabled: Bool, - pin: Bool + pin: Bool, + folderIds: [Int64] ) { self.sendAsPeerId = sendAsPeerId self.privacy = privacy self.timeout = timeout self.isForwardingDisabled = isForwardingDisabled self.pin = pin + self.folderIds = folderIds } public init(from decoder: Decoder) throws { @@ -45,6 +49,7 @@ public struct MediaEditorResultPrivacy: Codable, Equatable { self.timeout = Int(try container.decode(Int32.self, forKey: .timeout)) self.isForwardingDisabled = try container.decodeIfPresent(Bool.self, forKey: .disableForwarding) ?? false self.pin = try container.decode(Bool.self, forKey: .archive) + self.folderIds = try container.decodeIfPresent([Int64].self, forKey: .folderIds) ?? [] } public func encode(to encoder: Encoder) throws { @@ -55,6 +60,7 @@ public struct MediaEditorResultPrivacy: Codable, Equatable { try container.encode(Int32(self.timeout), forKey: .timeout) try container.encode(self.isForwardingDisabled, forKey: .disableForwarding) try container.encode(self.pin, forKey: .archive) + try container.encode(self.folderIds, forKey: .folderIds) } } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 6a5eeb7683..58defa6352 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -2915,7 +2915,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 86400, isForwardingDisabled: false, - pin: true + pin: true, + folderIds: [] ) } @@ -6641,7 +6642,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID self.mediaAreas = [] self.caption = NSAttributedString() self.coverTimestamp = nil - self.options = MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false) + self.options = MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false, folderIds: []) self.stickers = [] self.randomId = 0 } @@ -6651,7 +6652,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID mediaAreas: [MediaArea] = [], caption: NSAttributedString = NSAttributedString(), coverTimestamp: Double? = nil, - options: MediaEditorResultPrivacy = MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false), + options: MediaEditorResultPrivacy = MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false, folderIds: []), stickers: [TelegramMediaFile] = [], randomId: Int64 = 0 ) { @@ -6760,7 +6761,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID privacy: initialPrivacy, timeout: 86400, isForwardingDisabled: false, - pin: false + pin: false, + folderIds: [] ) } } else { @@ -6771,9 +6773,9 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID ).start(next: { [weak self] state, peer in if let self, var privacy = state?.privacy { if case let .user(user) = peer, !user.isPremium && privacy.timeout != 86400 { - privacy = MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: privacy.privacy, timeout: 86400, isForwardingDisabled: privacy.isForwardingDisabled, pin: privacy.pin) + privacy = MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: privacy.privacy, timeout: 86400, isForwardingDisabled: privacy.isForwardingDisabled, pin: privacy.pin, folderIds: privacy.folderIds) } else { - privacy = MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: privacy.privacy, timeout: privacy.timeout, isForwardingDisabled: privacy.isForwardingDisabled, pin: privacy.pin) + privacy = MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: privacy.privacy, timeout: privacy.timeout, isForwardingDisabled: privacy.isForwardingDisabled, pin: privacy.pin, folderIds: privacy.folderIds) } self.state.privacy = privacy } @@ -6980,7 +6982,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID mentions: mentions, coverImage: coverImage, stateContext: stateContext, - completion: { [weak self] sendAsPeerId, privacy, allowScreenshots, pin, _, completed in + completion: { [weak self] sendAsPeerId, privacy, allowScreenshots, pin, _, folders, completed in guard let self else { return } @@ -6989,13 +6991,14 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID privacy: privacy, timeout: timeout, isForwardingDisabled: !allowScreenshots, - pin: pin + pin: pin, + folderIds: folders ) if completed { completion() } }, - editCategory: { [weak self] privacy, allowScreenshots, pin in + editCategory: { [weak self] privacy, allowScreenshots, pin, folders in guard let self else { return } @@ -7008,11 +7011,12 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID privacy: privacy, timeout: timeout, isForwardingDisabled: !allowScreenshots, - pin: pin + pin: pin, + folderIds: folders ), completion: completion) }) }, - editBlockedPeers: { [weak self] privacy, allowScreenshots, pin in + editBlockedPeers: { [weak self] privacy, allowScreenshots, pin, folders in guard let self else { return } @@ -7025,7 +7029,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID privacy: privacy, timeout: timeout, isForwardingDisabled: !allowScreenshots, - pin: pin + pin: pin, + folderIds: folders ), completion: completion) }) }, @@ -7078,7 +7083,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID allowScreenshots: !isForwardingDisabled, pin: pin, stateContext: stateContext, - completion: { [weak self] _, result, isForwardingDisabled, pin, peers, completed in + completion: { [weak self] _, result, isForwardingDisabled, pin, peers, folders, completed in guard let self, completed else { return } @@ -7093,8 +7098,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID completion(result) } }, - editCategory: { _, _, _ in }, - editBlockedPeers: { _, _, _ in } + editCategory: { _, _, _, _ in }, + editBlockedPeers: { _, _, _, _ in } ) controller.customModalStyleOverlayTransitionFactorUpdated = { [weak self, weak controller] transition in if let self, let controller { @@ -7175,7 +7180,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID privacy: self.state.privacy.privacy, timeout: timeout ?? 86400, isForwardingDisabled: self.state.privacy.isForwardingDisabled, - pin: self.state.privacy.pin + pin: self.state.privacy.pin, + folderIds: self.state.privacy.folderIds ) } @@ -7672,7 +7678,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID mediaAreas: [], caption: NSAttributedString(), coverTimestamp: nil, - options: MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false), + options: MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false, folderIds: []), stickers: [], randomId: 0 )], { [weak self] finished in diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift index adab3a2d00..4197ba7446 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift @@ -527,6 +527,7 @@ private final class PeerInfoPendingPane { chatLocation: ChatLocation, chatLocationContextHolder: Atomic, sharedMediaFromForumTopic: (EnginePeer.Id, Int64)?, + initialStoryFolderId: Int64?, key: PeerInfoPaneKey, hasBecomeReady: @escaping (PeerInfoPaneKey) -> Void, parentController: ViewController?, @@ -609,7 +610,7 @@ private final class PeerInfoPendingPane { listContext = data.storyListContext } - let visualPaneNode = PeerInfoStoryPaneNode(context: context, scope: scope, captureProtected: captureProtected, isProfileEmbedded: true, canManageStories: canManage, navigationController: chatControllerInteraction.navigationController, listContext: listContext) + let visualPaneNode = PeerInfoStoryPaneNode(context: context, scope: scope, captureProtected: captureProtected, isProfileEmbedded: true, canManageStories: canManage, navigationController: chatControllerInteraction.navigationController, listContext: listContext, initialStoryFolderId: initialStoryFolderId) paneNode = visualPaneNode visualPaneNode.openCurrentDate = { openMediaCalendar() @@ -728,6 +729,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat private var currentPanes: [PeerInfoPaneKey: PeerInfoPaneWrapper] = [:] private var pendingPanes: [PeerInfoPaneKey: PeerInfoPendingPane] = [:] private var shouldFadeIn = false + private var initialStoryFolderId: Int64? private var transitionFraction: CGFloat = 0.0 @@ -753,7 +755,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat private let initialPaneKey: PeerInfoPaneKey? - init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: PeerId, chatLocation: ChatLocation, sharedMediaFromForumTopic: (EnginePeer.Id, Int64)?, chatLocationContextHolder: Atomic, isMediaOnly: Bool, initialPaneKey: PeerInfoPaneKey?) { + init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: PeerId, chatLocation: ChatLocation, sharedMediaFromForumTopic: (EnginePeer.Id, Int64)?, chatLocationContextHolder: Atomic, isMediaOnly: Bool, initialPaneKey: PeerInfoPaneKey?, initialStoryFolderId: Int64?) { self.context = context self.updatedPresentationData = updatedPresentationData self.peerId = peerId @@ -762,6 +764,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat self.sharedMediaFromForumTopic = sharedMediaFromForumTopic self.isMediaOnly = isMediaOnly self.initialPaneKey = initialPaneKey + self.initialStoryFolderId = initialStoryFolderId self.additionalBackgroundNode = ASDisplayNode() @@ -1088,7 +1091,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat } } } - if let pendingSwitchToPaneKey = self.pendingSwitchToPaneKey { + if let pendingSwitchToPaneKey = self.pendingSwitchToPaneKey, availablePanes.contains(pendingSwitchToPaneKey) { if self.currentPanes[pendingSwitchToPaneKey] == nil && self.pendingPanes[pendingSwitchToPaneKey] == nil { if !requiredPendingKeys.contains(pendingSwitchToPaneKey) { requiredPendingKeys.append(pendingSwitchToPaneKey) @@ -1099,6 +1102,13 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat for key in requiredPendingKeys { if self.pendingPanes[key] == nil, let data { var leftScope = false + var initialStoryFolderId: Int64? + if case .stories = key { + if let initialStoryFolderIdValue = self.initialStoryFolderId { + self.initialStoryFolderId = nil + initialStoryFolderId = initialStoryFolderIdValue + } + } let pane = PeerInfoPendingPane( context: self.context, updatedPresentationData: self.updatedPresentationData, @@ -1117,6 +1127,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, sharedMediaFromForumTopic: self.sharedMediaFromForumTopic, + initialStoryFolderId: initialStoryFolderId, key: key, hasBecomeReady: { [weak self] key in let apply: () -> Void = { @@ -1352,7 +1363,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat var removeKeys: [PeerInfoPaneKey] = [] for (key, paneNode) in self.pendingPanes { - if !availablePanes.contains(key) { + if !availablePanes.contains(key) && self.pendingSwitchToPaneKey != key { removeKeys.append(key) paneNode.pane.node.removeFromSupernode() } @@ -1363,7 +1374,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat removeKeys.removeAll() for (key, paneNode) in self.currentPanes { - if !availablePanes.contains(key) { + if !availablePanes.contains(key) && self.pendingSwitchToPaneKey != key { removeKeys.append(key) paneNode.node.removeFromSupernode() } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index a1223f9d14..7ae3a0bd34 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -2907,6 +2907,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro private let callMessages: [Message] private let chatLocation: ChatLocation private let chatLocationContextHolder: Atomic + private let switchToStoryFolder: Int64? private let sharedMediaFromForumTopic: (EnginePeer.Id, Int64)? let isSettings: Bool @@ -3031,7 +3032,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } private var didSetReady = false - init(controller: PeerInfoScreenImpl, context: AccountContext, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], isSettings: Bool, isMyProfile: Bool, hintGroupInCommon: PeerId?, requestsContext: PeerInvitationImportersContext?, profileGiftsContext: ProfileGiftsContext?, starsContext: StarsContext?, tonContext: StarsContext?, chatLocation: ChatLocation, chatLocationContextHolder: Atomic, initialPaneKey: PeerInfoPaneKey?, sharedMediaFromForumTopic: (EnginePeer.Id, Int64)?) { + init(controller: PeerInfoScreenImpl, context: AccountContext, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], isSettings: Bool, isMyProfile: Bool, hintGroupInCommon: PeerId?, requestsContext: PeerInvitationImportersContext?, profileGiftsContext: ProfileGiftsContext?, starsContext: StarsContext?, tonContext: StarsContext?, chatLocation: ChatLocation, chatLocationContextHolder: Atomic, switchToStoryFolder: Int64?, initialPaneKey: PeerInfoPaneKey?, sharedMediaFromForumTopic: (EnginePeer.Id, Int64)?) { self.controller = controller self.context = context self.peerId = peerId @@ -3047,6 +3048,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.chatLocationContextHolder = chatLocationContextHolder self.isMediaOnly = context.account.peerId == peerId && !isSettings && !isMyProfile self.initialExpandPanes = initialPaneKey != nil + self.switchToStoryFolder = switchToStoryFolder self.sharedMediaFromForumTopic = sharedMediaFromForumTopic self.scrollNode = ASScrollNode() @@ -3058,7 +3060,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro forumTopicThreadId = message.threadId } self.headerNode = PeerInfoHeaderNode(context: context, controller: controller, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, isMediaOnly: self.isMediaOnly, isSettings: isSettings, isMyProfile: isMyProfile, forumTopicThreadId: forumTopicThreadId, chatLocation: self.chatLocation) - self.paneContainerNode = PeerInfoPaneContainerNode(context: context, updatedPresentationData: controller.updatedPresentationData, peerId: peerId, chatLocation: chatLocation, sharedMediaFromForumTopic: sharedMediaFromForumTopic, chatLocationContextHolder: chatLocationContextHolder, isMediaOnly: self.isMediaOnly, initialPaneKey: initialPaneKey) + self.paneContainerNode = PeerInfoPaneContainerNode(context: context, updatedPresentationData: controller.updatedPresentationData, peerId: peerId, chatLocation: chatLocation, sharedMediaFromForumTopic: sharedMediaFromForumTopic, chatLocationContextHolder: chatLocationContextHolder, isMediaOnly: self.isMediaOnly, initialPaneKey: initialPaneKey, initialStoryFolderId: switchToStoryFolder) super.init() @@ -5393,7 +5395,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring) self.headerNode.updateIsAvatarExpanded(false, transition: transition) - self.updateNavigationExpansionPresentation(isExpanded: false, animated: true) + self.updateNavigationExpansionPresentation(isExpanded: false, animated: animated) if let (layout, navigationHeight) = self.validLayout { self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: transition, additive: true) @@ -5404,7 +5406,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let contentOffset = self.scrollNode.view.contentOffset let paneAreaExpansionFinalPoint: CGFloat = self.paneContainerNode.frame.minY - navigationHeight if contentOffset.y < paneAreaExpansionFinalPoint - CGFloat.ulpOfOne { - let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate self.ignoreScrolling = true transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: paneAreaExpansionFinalPoint), size: self.scrollNode.bounds.size)) @@ -11712,7 +11714,23 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } }))) - /*if pane.canReorder() { + if let _ = pane.currentStoryFolder { + items.append(.action(ContextMenuActionItem(text: "Share", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) + }, action: { [weak pane] _, a in + if ignoreNextActions { + return + } + ignoreNextActions = true + a(.default) + + if let pane { + pane.shareCurrentFolder() + } + }))) + } + + if pane.canReorder() { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.BotPreviews_MenuReorder, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { [weak pane] _, a in @@ -11744,7 +11762,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro pane.presentDeleteCurrentStoryFolder() } }))) - }*/ + } if let language = pane.currentBotPreviewLanguage { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.BotPreviews_MenuDeleteLanguage(language.name).string, textColor: .destructive, icon: { theme in @@ -13084,6 +13102,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc private let switchToRecommendedChannels: Bool private let switchToGifts: Bool private let switchToGroupsInCommon: Bool + private let switchToStoryFolder: Int64? private let sharedMediaFromForumTopic: (EnginePeer.Id, Int64)? let chatLocation: ChatLocation private let chatLocationContextHolder = Atomic(value: nil) @@ -13159,7 +13178,8 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc sharedMediaFromForumTopic: (EnginePeer.Id, Int64)? = nil, switchToRecommendedChannels: Bool = false, switchToGifts: Bool = false, - switchToGroupsInCommon: Bool = false + switchToGroupsInCommon: Bool = false, + switchToStoryFolder: Int64? = nil ) { self.context = context self.updatedPresentationData = updatedPresentationData @@ -13177,6 +13197,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc self.switchToRecommendedChannels = switchToRecommendedChannels self.switchToGifts = switchToGifts self.switchToGroupsInCommon = switchToGroupsInCommon + self.switchToStoryFolder = switchToStoryFolder self.sharedMediaFromForumTopic = sharedMediaFromForumTopic if let forumTopicThread = forumTopicThread { @@ -13542,8 +13563,10 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc initialPaneKey = .gifts } else if self.switchToGroupsInCommon { initialPaneKey = .groupsInCommon + } else if self.switchToStoryFolder != nil { + initialPaneKey = .stories } - self.displayNode = PeerInfoScreenNode(controller: self, context: self.context, peerId: self.peerId, avatarInitiallyExpanded: self.avatarInitiallyExpanded, isOpenedFromChat: self.isOpenedFromChat, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, isSettings: self.isSettings, isMyProfile: self.isMyProfile, hintGroupInCommon: self.hintGroupInCommon, requestsContext: self.requestsContext, profileGiftsContext: self.profileGiftsContext, starsContext: self.starsContext, tonContext: self.tonContext, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, initialPaneKey: initialPaneKey, sharedMediaFromForumTopic: self.sharedMediaFromForumTopic) + self.displayNode = PeerInfoScreenNode(controller: self, context: self.context, peerId: self.peerId, avatarInitiallyExpanded: self.avatarInitiallyExpanded, isOpenedFromChat: self.isOpenedFromChat, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, isSettings: self.isSettings, isMyProfile: self.isMyProfile, hintGroupInCommon: self.hintGroupInCommon, requestsContext: self.requestsContext, profileGiftsContext: self.profileGiftsContext, starsContext: self.starsContext, tonContext: self.tonContext, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, switchToStoryFolder: self.switchToStoryFolder, initialPaneKey: initialPaneKey, sharedMediaFromForumTopic: self.sharedMediaFromForumTopic) self.controllerNode.accountsAndPeers.set(self.accountsAndPeers.get() |> map { $0.1 }) self.controllerNode.activeSessionsContextAndCount.set(self.activeSessionsContextAndCount.get()) self.cachedDataPromise.set(self.controllerNode.cachedDataPromise.get()) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift index efce1ee57d..edc90afb81 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift @@ -488,7 +488,8 @@ final class PeerInfoStoryGridScreenComponent: Component { } return self.environment?.controller()?.navigationController as? NavigationController }, - listContext: nil + listContext: nil, + initialStoryFolderId: nil ) paneNode.isEmptyUpdated = { [weak self] _ in guard let self else { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift index b6d163bd78..025a670357 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift @@ -117,7 +117,8 @@ final class StorySearchGridScreenComponent: Component { } return self.environment?.controller()?.navigationController as? NavigationController }, - listContext: component.listContext + listContext: component.listContext, + initialStoryFolderId: nil ) paneNode.parentController = environment.controller() paneNode.isEmptyUpdated = { [weak self] _ in diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 4335317e2d..2583e67945 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -44,6 +44,8 @@ import MultilineTextComponent import LocationUI import TabSelectorComponent import LanguageSelectionScreen +import PromptUI +import BottomButtonPanelComponent private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6) private let mediaBadgeTextColor = UIColor.white @@ -1568,7 +1570,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr private var mapInfoNode: LocationInfoListItemNode? private var searchHeader: ComponentView? - private var botPreviewLanguageTab: ComponentView? + private var folderTab: ComponentView? private var botPreviewFooter: ComponentView? private var barBackgroundLayer: SimpleLayer? @@ -1581,8 +1583,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr private var reorderedIds: [StoryId]? private var itemCount: Int? private var didUpdateItemsOnce: Bool = false + private var itemTabId: AnyHashable? private var selectionPanel: ComponentView? + private var actionPanel: ComponentView? private var isDeceleratingAfterTracking = false @@ -1648,6 +1652,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr public var tabBarOffset: CGFloat { if case .botPreview = self.scope { return 0.0 + } else if case let .peer(peerId, _, isArchived) = self.scope, ((peerId == self.context.account.peerId && !isArchived) || !self.currentStoryFolders.isEmpty || self.initialStoryFolderId != nil), self.isProfileEmbedded { + return 0.0 } else { return self.itemGrid.coveringInsetOffset } @@ -1661,6 +1667,9 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr private var currentBotPreviewLanguages: [StoryListContext.State.Language] = [] private var removedBotPreviewLanguages = Set() + private var currentStoryFolders: [StoryListContext.State.Folder] = [] + private var removedStoryFolders = Set() + private var numberOfItemsToRequest: Int = 50 private var isRequestingView: Bool = false private var isFirstHistoryView: Bool = true @@ -1675,7 +1684,9 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr private let maxBotPreviewCount: Int private let defaultListSource: StoryListContext - private var cachedListSources: [String: StoryListContext] = [:] + private var cachedListSources: [AnyHashable: StoryListContext] = [:] + + private var initialStoryFolderId: Int64? public var currentBotPreviewLanguage: (id: String, name: String)? { guard let listSource = self.listSource as? BotPreviewStoryListContext else { @@ -1689,6 +1700,19 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } return (language.id, language.name) } + + public var currentStoryFolder: (id: Int64, title: String)? { + guard let listSource = self.listSource as? PeerStoryListContext else { + return nil + } + guard let id = listSource.folderId else { + return nil + } + guard let folder = self.currentStoryFolders.first(where: { $0.id == id }) else { + return nil + } + return (folder.id, folder.title) + } public var openCurrentDate: (() -> Void)? public var paneDidScroll: (() -> Void)? @@ -1719,12 +1743,13 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr return 0.0 } - public init(context: AccountContext, scope: Scope, captureProtected: Bool, isProfileEmbedded: Bool, canManageStories: Bool, navigationController: @escaping () -> NavigationController?, listContext: StoryListContext?) { + public init(context: AccountContext, scope: Scope, captureProtected: Bool, isProfileEmbedded: Bool, canManageStories: Bool, navigationController: @escaping () -> NavigationController?, listContext: StoryListContext?, initialStoryFolderId: Int64?) { self.context = context self.scope = scope self.navigationController = navigationController self.isProfileEmbedded = isProfileEmbedded self.canManageStories = canManageStories + self.initialStoryFolderId = initialStoryFolderId switch scope { case let .peer(_, _, isArchived): @@ -1800,7 +1825,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr return } - if self.isProfileEmbedded { + if self.isProfileEmbedded || !self.shouldOpenItemsWhileInSelectionMode { if let selectedIds = self.itemInteraction.selectedIds { self.itemInteraction.toggleSelection(item.story.id, !selectedIds.contains(item.story.id)) return @@ -2449,6 +2474,122 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr var items: [ContextMenuItem] = [] if canManage, case let .peer(peerId, _, isArchived) = self.scope { + if !isArchived && peerId == self.context.account.peerId && self.isProfileEmbedded { + if let folder = self.currentStoryFolder { + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Remove from Album", textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] _, f in + guard let self else { + f(.default) + return + } + + if let listSource = self.listSource as? PeerStoryListContext { + let _ = listSource.removeFromFolder(id: folder.id, itemIds: [item.id]) + } + + f(.dismissWithoutContent) + }))) + } else { + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Add to Album", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddToFolder"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in + guard let self, let c else { + f(.default) + return + } + + Task { @MainActor [weak self, weak c] in + guard let self, let c else { + return + } + + let (peerReference, folderPreviews) = await PeerStoryListContext.folderPreviews(peerId: peerId, account: self.context.account).get() + + var items: [ContextMenuItem] = [] + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Common_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) + }, iconPosition: .left, action: { c ,f in + c?.popItems() + }))) + items.append(.separator) + + items.append(.action(ContextMenuActionItem(text: "New Album", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddFolder"), color: theme.contextMenu.primaryColor) }, iconPosition: .left, action: { [weak self] c, f in + guard let self else { + f(.default) + return + } + + c?.dismiss(completion: { [weak self] in + guard let self else { + return + } + self.presentAddStoryFolder(addItems: [item]) + }) + }))) + + for folderPreview in folderPreviews { + var iconSource: ContextMenuActionItemIconSource? + if let story = folderPreview.item { + var imageSignal: Signal? + + var selectedMedia: Media? + if let image = story.media._asMedia() as? TelegramMediaImage { + selectedMedia = image + } else if let file = story.media._asMedia() as? TelegramMediaFile { + selectedMedia = file + } + + if let selectedMedia { + if let result = self.directMediaImageCache.getImage(peer: peerReference, story: story, media: selectedMedia, width: 24, aspectRatio: 1.0, possibleWidths: [24], includeBlurred: false, synchronous: true) { + if let loadSignal = result.loadSignal { + imageSignal = .single(result.image) |> then(loadSignal) + } else { + imageSignal = .single(result.image) + } + } + } + + if let imageSignal { + iconSource = ContextMenuActionItemIconSource( + size: CGSize(width: 24.0, height: 24.0), + cornerRadius: 5.0, + signal: imageSignal + ) + } + } + + var icon: (PresentationTheme) -> UIImage? = { _ in nil } + if iconSource == nil { + icon = { theme in + return generateImage(CGSize(width: 24.0, height: 24.0), opaque: false, scale: nil, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.contextMenu.primaryColor.withMultipliedAlpha(0.1).cgColor) + context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 5.0).cgPath) + context.fillPath() + }) + } + } + + items.append(.action(ContextMenuActionItem(text: folderPreview.folder.title, icon: icon, iconSource: iconSource, iconPosition: .left, action: { [weak self] c, f in + guard let self else { + f(.default) + return + } + + c?.dismiss(completion: {}) + + if let listSource = self.listSource as? PeerStoryListContext { + let _ = listSource.addToFolder(id: folderPreview.folder.id, items: [item]) + } + }))) + } + + c.pushItems(items: .single(ContextController.Items(content: .list(items)))) + } + }))) + } + items.append(.separator) + } + items.append(.action(ContextMenuActionItem(text: !isArchived ? self.presentationData.strings.StoryList_ItemAction_Archive : self.presentationData.strings.StoryList_ItemAction_Unarchive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isArchived ? "Chat/Context Menu/Archive" : "Chat/Context Menu/Unarchive"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in guard let self else { f(.default) @@ -2713,7 +2854,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr public func ensureMessageIsVisible(id: MessageId) { } - private func requestHistoryAroundVisiblePosition(synchronous: Bool, reloadAtTop: Bool) { + private func requestHistoryAroundVisiblePosition(synchronous: Bool, reloadAtTop: Bool, animated: Bool = true) { if self.isRequestingView { return } @@ -2745,7 +2886,12 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr title = self.presentationData.strings.BotPreviews_SubtitleEmpty } } else { - title = "" + if state.isLoading { + title = self.presentationData.strings.BotPreviews_SubtitleLoading + } else { + //TODO:localize + title = "no stories" + } } } else if case let .peer(_, isSaved, isArchived) = self.scope { if isSaved { @@ -2786,6 +2932,16 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } botPreviewLanguages.sort(by: { $0.name < $1.name }) + var storyFolders = self.currentStoryFolders + for folder in state.availableFolders { + if !storyFolders.contains(where: { $0.id == folder.id }) && !self.removedStoryFolders.contains(folder.id) { + storyFolders.append(folder) + } + if let index = storyFolders.firstIndex(where: { $0.id == folder.id }) { + storyFolders[index] = folder + } + } + var hadLocalItems = false if let currentListState = self.currentListState { for item in currentListState.items { @@ -2795,33 +2951,41 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } } - self.currentListState = state - - var hasLocalItems = false - if let currentListState = self.currentListState { - for item in currentListState.items { - if item.storyItem.isPending { - hasLocalItems = true + if let initialStoryFolderId = self.initialStoryFolderId, let folder = storyFolders.first(where: { $0.id == initialStoryFolderId }) { + self.currentStoryFolders = storyFolders + self.isRequestingView = false + self.initialStoryFolderId = nil + self.setStoryFolder(id: folder.id, assumeEmpty: false, animated: false) + } else { + self.currentListState = state + + var hasLocalItems = false + if let currentListState = self.currentListState { + for item in currentListState.items { + if item.storyItem.isPending { + hasLocalItems = true + } } } - } - - var synchronous = synchronous - if hasLocalItems != hadLocalItems { - synchronous = true - } - - self.updateItemsFromState(state: state, firstTime: firstTime, reloadAtTop: reloadAtTop, synchronous: synchronous, animated: false) - - if self.currentBotPreviewLanguages != botPreviewLanguages || reloadAtTop { - self.currentBotPreviewLanguages = botPreviewLanguages - if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams { - self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: synchronous, transition: .immediate) + + var synchronous = synchronous + if hasLocalItems != hadLocalItems { + synchronous = true } + + self.updateItemsFromState(state: state, firstTime: firstTime, reloadAtTop: reloadAtTop, synchronous: synchronous, animated: false) + + if self.currentBotPreviewLanguages != botPreviewLanguages || self.currentStoryFolders != storyFolders || reloadAtTop { + self.currentBotPreviewLanguages = botPreviewLanguages + self.currentStoryFolders = storyFolders + if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams { + self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: synchronous, transition: .immediate) + } + } + + firstTime = false + self.isRequestingView = false } - - firstTime = false - self.isRequestingView = false } }) } @@ -2870,9 +3034,13 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr switch self.scope { case .botPreview: isReorderable = !item.storyItem.isPending - case let .peer(id, _, _): - if id == self.context.account.peerId { - isReorderable = state.pinnedIds.contains(item.storyItem.id) + case .peer: + if self.canManageStories { + if self.currentStoryFolder != nil { + isReorderable = true + } else { + isReorderable = state.pinnedIds.contains(item.storyItem.id) + } } case let .search(peerId, _): if peerId != nil { @@ -2899,7 +3067,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr totalCount = state.totalCount totalCount = max(mappedItems.count, totalCount) - if totalCount == 0 && state.loadMoreToken != nil && !state.isCached { + if totalCount == 0 && state.loadMoreToken != nil && (!state.isCached || !state.hasCache) { totalCount = 100 } @@ -2920,13 +3088,40 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr ) self.itemCount = state.totalCount + + var itemTabId: AnyHashable? + if let folder = self.currentStoryFolder { + itemTabId = AnyHashable(folder.id) + } + let previousItemTabId = self.itemTabId + self.itemTabId = itemTabId + + var animateDirection: Bool? + if firstTime { + if previousItemTabId == nil && itemTabId == nil { + } else if let previousItemTabId, let itemTabId { + let previousIndex = self.currentStoryFolders.firstIndex(where: { AnyHashable($0.id) == previousItemTabId }) + let updatedIndex = self.currentStoryFolders.firstIndex(where: { AnyHashable($0.id) == itemTabId }) + if let previousIndex, let updatedIndex { + animateDirection = updatedIndex > previousIndex + } + } else { + if previousItemTabId != nil { + if itemTabId == nil { + animateDirection = false + } + } else if itemTabId != nil { + animateDirection = true + } + } + } let currentSynchronous = synchronous && firstTime let currentReloadAtTop = reloadAtTop && firstTime - self.updateHistory(items: items, pinnedIds: Set(state.pinnedIds), synchronous: currentSynchronous, reloadAtTop: currentReloadAtTop, animated: animated) + self.updateHistory(items: items, pinnedIds: Set(state.pinnedIds), synchronous: currentSynchronous, reloadAtTop: currentReloadAtTop, animated: animated, animateDirection: animateDirection) } - private func updateHistory(items: SparseItemGrid.Items, pinnedIds: Set, synchronous: Bool, reloadAtTop: Bool, animated: Bool) { + private func updateHistory(items: SparseItemGrid.Items, pinnedIds: Set, synchronous: Bool, reloadAtTop: Bool, animated: Bool, animateDirection: Bool?) { var transition: ContainedViewLayoutTransition = .immediate if case .location = self.scope, let previousItems = self.items, previousItems.items.count == 0, previousItems.count != 0, items.items.count == 0, items.count == 0 { transition = .animated(duration: 0.3, curve: .spring) @@ -2937,18 +3132,58 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams { var gridSnapshot: UIView? - if case .botPreview = scope { - } else if reloadAtTop { + var emptyStateSnapshot: UIView? + if animateDirection != nil { gridSnapshot = self.itemGrid.view.snapshotView(afterScreenUpdates: false) + + if let emptyStateView = self.emptyStateView?.view { + emptyStateSnapshot = emptyStateView.snapshotView(afterScreenUpdates: false) + emptyStateSnapshot?.frame = emptyStateView.frame + } + } else { + if case .botPreview = self.scope { + } else if !self.currentStoryFolders.isEmpty { + } else if case let .peer(id, _, isArchived) = self.scope, ((id == self.context.account.peerId && !isArchived) || !self.currentStoryFolders.isEmpty || self.initialStoryFolderId != nil) { + } else if reloadAtTop { + gridSnapshot = self.itemGrid.view.snapshotView(afterScreenUpdates: false) + } } - self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: false, transition: transition, animateGridItems: animated) + + self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: false, transition: transition, animateGridItems: animated, animateBottomPanel: animateDirection != nil) 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 - gridSnapshot?.removeFromSuperview() + + if let gridSnapshot { + self.view.insertSubview(gridSnapshot, aboveSubview: self.contextGestureContainerNode.view) + gridSnapshot.frame = self.itemGrid.frame + if let animateDirection { + let directionFactor: CGFloat = animateDirection ? 1.0 : -1.0 + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) + transition.animatePositionAdditive(node: self.itemGrid, offset: CGPoint(x: size.width * directionFactor, y: 0.0)) + transition.animatePosition(layer: gridSnapshot.layer, from: CGPoint(), to: CGPoint(x: size.width * (-directionFactor), y: 0.0), removeOnCompletion: false, additive: true, completion: { [weak gridSnapshot] _ in + gridSnapshot?.removeFromSuperview() + }) + } else { + gridSnapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak gridSnapshot] _ in + gridSnapshot?.removeFromSuperview() + }) + } + } + if let emptyStateSnapshot, let animateDirection { + self.view.insertSubview(emptyStateSnapshot, belowSubview: self.contextGestureContainerNode.view) + let directionFactor: CGFloat = animateDirection ? 1.0 : -1.0 + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) + transition.animatePosition(layer: emptyStateSnapshot.layer, from: CGPoint(), to: CGPoint(x: size.width * (-directionFactor), y: 0.0), removeOnCompletion: false, additive: true, completion: { [weak emptyStateSnapshot] _ in + emptyStateSnapshot?.removeFromSuperview() }) } + if let emptyStateView = self.emptyStateView?.view, let animateDirection { + let directionFactor: CGFloat = animateDirection ? 1.0 : -1.0 + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) + transition.animatePositionAdditive(layer: emptyStateView.layer, offset: CGPoint(x: size.width * directionFactor, y: 0.0)) + } } self.isEmptyUpdated(self.isEmpty) @@ -2965,11 +3200,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if case .botPreview = self.scope { } else if case let .peer(id, _, _) = self.scope { if id == self.context.account.peerId { - let maxPinnedIndex = items.items.lastIndex(where: { ($0 as? VisualMediaItem)?.isPinned == true }) - if let maxPinnedIndex { - toIndex = min(toIndex, maxPinnedIndex) + if self.currentStoryFolder != nil { } else { - return + let maxPinnedIndex = items.items.lastIndex(where: { ($0 as? VisualMediaItem)?.isPinned == true }) + if let maxPinnedIndex { + toIndex = min(toIndex, maxPinnedIndex) + } else { + return + } } } } else { @@ -3381,8 +3619,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if let _ = self.mapNode { self.updateMapLayout(size: currentParams.size, topInset: currentParams.topInset, bottomInset: currentParams.bottomInset, deviceMetrics: currentParams.deviceMetrics, transition: transition) } - if case .botPreview = self.scope, self.canManageStories { - self.updateBotPreviewLanguageTab(size: currentParams.size, topInset: currentParams.topInset, transition: transition) + if case let .peer(peerId, _, isArchived) = self.scope, ((peerId == self.context.account.peerId && !isArchived) || !self.currentStoryFolders.isEmpty), self.isProfileEmbedded { + self.updateFolderTab(size: currentParams.size, topInset: currentParams.topInset, transition: transition) + } else if case .botPreview = self.scope, self.canManageStories { + self.updateFolderTab(size: currentParams.size, topInset: currentParams.topInset, transition: transition) self.updateBotPreviewFooter(size: currentParams.size, bottomInset: 0.0, transition: transition) } } @@ -3538,40 +3778,172 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } } - private func updateBotPreviewLanguageTab(size: CGSize, topInset: CGFloat, transition: ContainedViewLayoutTransition) { - guard case .botPreview = self.scope, self.canManageStories else { + private func updateFolderTab(size: CGSize, topInset: CGFloat, transition: ContainedViewLayoutTransition) { + var displayFolderTab = false + if case .botPreview = self.scope, self.canManageStories { + displayFolderTab = true + } else if case let .peer(peerId, _, isArchived) = self.scope, ((peerId == self.context.account.peerId && !isArchived) || !self.currentStoryFolders.isEmpty), self.isProfileEmbedded { + displayFolderTab = true + } + + if !displayFolderTab { return } - let botPreviewLanguageTab: ComponentView - if let current = self.botPreviewLanguageTab { - botPreviewLanguageTab = current + var transition = transition + + let folderTab: ComponentView + if let current = self.folderTab { + folderTab = current } else { - botPreviewLanguageTab = ComponentView() - self.botPreviewLanguageTab = botPreviewLanguageTab + transition = .immediate + folderTab = ComponentView() + self.folderTab = folderTab } - var languageItems: [TabSelectorComponent.Item] = [] - languageItems.append(TabSelectorComponent.Item( + var folderItems: [TabSelectorComponent.Item] = [] + let mainTitle: String + let addTitle: String + if case .botPreview = self.scope { + mainTitle = self.presentationData.strings.BotPreviews_LanguageTab_Main + addTitle = self.presentationData.strings.BotPreviews_LanguageTab_Add + } else { + //TODO:localize + mainTitle = "All Stories" + addTitle = "+ Add Album" + } + folderItems.append(TabSelectorComponent.Item( id: AnyHashable("_main"), - title: self.presentationData.strings.BotPreviews_LanguageTab_Main + title: mainTitle )) - for language in self.currentBotPreviewLanguages { - languageItems.append(TabSelectorComponent.Item( - id: AnyHashable(language.id), - title: language.name + + if case .botPreview = self.scope { + for language in self.currentBotPreviewLanguages { + folderItems.append(TabSelectorComponent.Item( + id: AnyHashable(language.id), + title: language.name + )) + } + } else { + for folder in self.currentStoryFolders { + folderItems.append(TabSelectorComponent.Item( + id: AnyHashable(folder.id), + title: folder.title, + isReorderable: self.canManageStories, + contextAction: { [weak self] sourceNode, gesture in + guard let self else { + return + } + guard let sourceNode = sourceNode as? ContextExtractedContentContainingNode else { + return + } + guard let controller = self.parentController else { + return + } + + var items: [ContextMenuItem] = [] + + if self.canManageStories { + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Add Stories", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/AddStoryIcon"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + guard let self else { + a(.default) + return + } + + a(.default) + + self.presentAddStoriesToFolder() + }))) + } + + if self.canManageStories { + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Rename Album", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + guard let self else { + f(.default) + return + } + + f(.dismissWithoutContent) + + self.presentRenameStoryFolder(id: folder.id, title: folder.title) + }))) + + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Share", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + guard let self else { + f(.default) + return + } + + f(.dismissWithoutContent) + + self.shareFolder(id: folder.id) + }))) + } + + if self.canManageStories { + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + guard let self else { + a(.default) + return + } + + a(.default) + + self.beginReordering() + }))) + + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Delete Album", textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] _, f in + guard let self else { + f(.default) + return + } + + f(.dismissWithoutContent) + + self.presentDeleteStoryFolder(id: folder.id) + }))) + } + + let presentationData = self.presentationData + let contextController = ContextController( + presentationData: presentationData, + source: .extracted(ItemExtractedContentSource( + sourceNode: sourceNode, + containerView: controller.view, + keepInPlace: false + )), + items: .single(ContextController.Items(content: .list(items))), + recognizer: nil, + gesture: gesture + ) + controller.presentInGlobalOverlay(contextController) + } + )) + } + } + if self.canManageStories { + folderItems.append(TabSelectorComponent.Item( + id: AnyHashable("_add"), + title: addTitle )) } - languageItems.append(TabSelectorComponent.Item( - id: AnyHashable("_add"), - title: self.presentationData.strings.BotPreviews_LanguageTab_Add - )) - var selectedLanguageId = "_main" + var selectedId = AnyHashable("_main") if let listSource = self.listSource as? BotPreviewStoryListContext, let language = listSource.language { - selectedLanguageId = language + selectedId = AnyHashable(language) + } else if let listSource = self.listSource as? PeerStoryListContext, let folderId = listSource.folderId { + selectedId = AnyHashable(folderId) } - let botPreviewLanguageTabSize = botPreviewLanguageTab.update( + let folderTabSize = folderTab.update( transition: ComponentTransition(transition), component: AnyComponent(TabSelectorComponent( colors: TabSelectorComponent.Colors( @@ -3584,39 +3956,95 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr spacing: 9.0, verticalInset: 11.0 ), - items: languageItems, - selectedId: AnyHashable(selectedLanguageId), - setSelectedId: { [weak self] id in - guard let self, let id = id.base as? String else { + items: folderItems, + selectedId: selectedId, + reorderItem: self.isReordering ? { [weak self] fromId, toId in + guard let self else { return } - if id == "_add" { - self.presentAddBotPreviewLanguage() - } else if id == "_main" { - self.setBotPreviewLanguage(id: nil, assumeEmpty: false) - } else if let language = self.currentBotPreviewLanguages.first(where: { $0.id == id }) { - self.setBotPreviewLanguage(id: language.id, assumeEmpty: false) + guard let sourceId = fromId.base as? Int64 else { + return + } + guard let targetId = toId.base as? Int64 else { + return + } + guard let sourceIndex = self.currentStoryFolders.firstIndex(where: { $0.id == sourceId }), let targetIndex = self.currentStoryFolders.firstIndex(where: { $0.id == targetId }) else { + return + } + let folder = self.currentStoryFolders[sourceIndex] + if targetIndex < sourceIndex { + self.currentStoryFolders.remove(at: sourceIndex) + self.currentStoryFolders.insert(folder, at: targetIndex) + } else { + self.currentStoryFolders.insert(folder, at: targetIndex + 1) + self.currentStoryFolders.remove(at: sourceIndex) + } + + self.update(transition: .animated(duration: 0.2, curve: .easeInOut)) + } : nil, + setSelectedId: { [weak self] id in + guard let self else { + return + } + + self.expandIfNeeded?() + + if let id = id.base as? String { + if id == "_add" { + if case .botPreview = self.scope { + self.presentAddBotPreviewLanguage() + } else { + self.presentAddStoryFolder() + } + } else if id == "_main" { + if case .botPreview = self.scope { + self.setBotPreviewLanguage(id: nil, assumeEmpty: false) + } else { + self.setStoryFolder(id: nil, assumeEmpty: false) + } + } else if let language = self.currentBotPreviewLanguages.first(where: { $0.id == id }) { + self.setBotPreviewLanguage(id: language.id, assumeEmpty: false) + } + } else if let id = id.base as? Int64 { + if let folder = self.currentStoryFolders.first(where: { $0.id == id }) { + self.setStoryFolder(id: folder.id, assumeEmpty: false) + } } } )), environment: {}, containerSize: CGSize(width: size.width, height: 44.0) ) - var botPreviewLanguageTabFrame = CGRect(origin: CGPoint(x: floor((size.width - botPreviewLanguageTabSize.width) * 0.5), y: topInset - 11.0), size: botPreviewLanguageTabSize) + var folderTabFrame = CGRect(origin: CGPoint(x: floor((size.width - folderTabSize.width) * 0.5), y: topInset - 11.0), size: folderTabSize) let effectiveScrollingOffset: CGFloat effectiveScrollingOffset = self.itemGrid.scrollingOffset - botPreviewLanguageTabFrame.origin.y -= effectiveScrollingOffset + folderTabFrame.origin.y -= effectiveScrollingOffset let isSelectingOrReordering = self.isReordering || self.itemInteraction.selectedIds != nil - if let botPreviewLanguageTabView = botPreviewLanguageTab.view { - if botPreviewLanguageTabView.superview == nil { - self.view.addSubview(botPreviewLanguageTabView) + if let folderTabView = folderTab.view { + if folderTabView.superview == nil { + self.view.addSubview(folderTabView) } - transition.updateFrame(view: botPreviewLanguageTabView, frame: botPreviewLanguageTabFrame) - transition.updateAlpha(layer: botPreviewLanguageTabView.layer, alpha: isSelectingOrReordering ? 0.5 : 1.0) - botPreviewLanguageTabView.isUserInteractionEnabled = !isSelectingOrReordering + transition.updateFrame(view: folderTabView, frame: folderTabFrame) + + var areTabsDisabled = false + + if case .botPreview = self.scope { + if isSelectingOrReordering { + areTabsDisabled = true + } + } else { + if self.itemInteraction.selectedIds != nil { + areTabsDisabled = true + } + } + + transition.updateAlpha(layer: folderTabView.layer, alpha: areTabsDisabled ? 0.5 : 1.0) + folderTabView.isUserInteractionEnabled = !areTabsDisabled + + folderTabView.disablesInteractiveTransitionGestureRecognizer = self.isReordering } } @@ -3692,10 +4120,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { - self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: synchronous, transition: transition, animateGridItems: false) + self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: synchronous, transition: transition, animateGridItems: false, animateBottomPanel: transition.isAnimated) } - private func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition, animateGridItems: Bool) { + private func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition, animateGridItems: Bool, animateBottomPanel: Bool) { self.currentParams = (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) var gridTopInset = topInset @@ -3729,7 +4157,15 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr mapOptionsNode.updateLayout(size: mapOptionsFrame.size, leftInset: sideInset, rightInset: sideInset, transition: transition) } - if self.isProfileEmbedded, case .botPreview = self.scope { + var hasBarBackground = false + if self.isProfileEmbedded { + if case .botPreview = self.scope { + hasBarBackground = true + } else if case let .peer(id, _, isArchived) = self.scope, ((id == self.context.account.peerId && !isArchived) || !self.currentStoryFolders.isEmpty) { + hasBarBackground = true + } + } + if hasBarBackground { let barBackgroundLayer: SimpleLayer if let current = self.barBackgroundLayer { barBackgroundLayer = current @@ -3745,15 +4181,29 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr var listBottomInset = bottomInset var bottomInset = bottomInset + var displayFolderTab = false if case .botPreview = self.scope, self.canManageStories { - updateBotPreviewLanguageTab(size: size, topInset: topInset, transition: transition) + displayFolderTab = true + } else if case let .peer(peerId, _, isArchived) = self.scope, ((peerId == self.context.account.peerId && !isArchived) || !self.currentStoryFolders.isEmpty || self.initialStoryFolderId != nil), self.isProfileEmbedded { + displayFolderTab = true + } + + if displayFolderTab { + var folderTabsTransition = transition + if animateBottomPanel && !folderTabsTransition.isAnimated { + folderTabsTransition = .animated(duration: 0.4, curve: .spring) + } + + updateFolderTab(size: size, topInset: topInset, transition: folderTabsTransition) gridTopInset += 50.0 - updateBotPreviewFooter(size: size, bottomInset: 0.0, transition: transition) - if let botPreviewFooterView = self.botPreviewFooter?.view { + if case .botPreview = self.scope { + updateBotPreviewFooter(size: size, bottomInset: 0.0, transition: transition) + if let botPreviewFooterView = self.botPreviewFooter?.view { listBottomInset += 18.0 + botPreviewFooterView.bounds.height } } + } if self.isProfileEmbedded, let selectedIds = self.itemInteraction.selectedIds, self.canManageStories, case let .peer(peerId, _, isArchived) = self.scope { let selectionPanel: ComponentView @@ -3933,13 +4383,206 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } else if let selectionPanel = self.selectionPanel { self.selectionPanel = nil if let selectionPanelView = selectionPanel.view { - transition.updateFrame(view: selectionPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height), size: selectionPanelView.bounds.size)) + transition.updateFrame(view: selectionPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height), size: selectionPanelView.bounds.size), completion: { [weak selectionPanelView] _ in + selectionPanelView?.removeFromSuperview() + }) + } + } + + var actionPanelGeneralTransition: ComponentTransition + if animateBottomPanel && !transition.isAnimated { + actionPanelGeneralTransition = .spring(duration: 0.4) + } else { + actionPanelGeneralTransition = ComponentTransition(transition) + } + if self.selectionPanel == nil, self.isProfileEmbedded, self.canManageStories, case let .peer(peerId, _, isArchived) = self.scope, peerId == self.context.account.peerId, !isArchived, self.isProfileEmbedded, self.currentStoryFolder != nil, let items = self.items, !items.items.isEmpty { + let actionPanel: ComponentView + var actionPanelTransition = ComponentTransition(transition) + if let current = self.actionPanel { + actionPanel = current + } else { + actionPanelTransition = actionPanelTransition.withAnimation(.none) + actionPanel = ComponentView() + self.actionPanel = actionPanel + } + + //TODO:localize + let actionPanelSize = actionPanel.update( + transition: actionPanelTransition, + component: AnyComponent(BottomButtonPanelComponent( + theme: presentationData.theme, + title: "Add Stories", + label: nil, + isEnabled: true, + insets: UIEdgeInsets(top: 0.0, left: sideInset + 12.0, bottom: bottomInset, right: sideInset + 12.0), + action: { [weak self] in + guard let self else { + return + } + self.presentAddStoriesToFolder() + } + )), + environment: {}, + containerSize: size + ) + let actionPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - actionPanelSize.height), size: actionPanelSize) + if let actionPanelView = actionPanel.view { + if actionPanelView.superview == nil { + self.view.addSubview(actionPanelView) + actionPanelGeneralTransition.animatePosition(layer: actionPanelView.layer, from: CGPoint(x: 0.0, y: actionPanelFrame.height), to: CGPoint(), additive: true) + } + actionPanelTransition.setFrame(view: actionPanelView, frame: actionPanelFrame) + } + bottomInset = actionPanelSize.height + listBottomInset += actionPanelSize.height + } else if let actionPanel = self.actionPanel { + self.actionPanel = nil + if let actionPanelView = actionPanel.view { + actionPanelGeneralTransition.setFrame(view: actionPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height), size: actionPanelView.bounds.size), completion: { [weak actionPanelView] _ in + actionPanelView?.removeFromSuperview() + }) } } transition.updateFrame(node: self.contextGestureContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) - if case let .peer(_, _, isArchived) = self.scope, let items = self.items, items.items.isEmpty, items.count == 0 { + if case let .peer(peerId, _, isArchived) = self.scope, let items = self.items, items.items.isEmpty, items.count == 0 { + if peerId == self.context.account.peerId, self.isProfileEmbedded, self.currentStoryFolder != nil { + let emptyStateView: ComponentView + var emptyStateTransition = ComponentTransition(transition) + if let current = self.emptyStateView { + emptyStateView = current + } else { + emptyStateTransition = .immediate + emptyStateView = ComponentView() + self.emptyStateView = emptyStateView + } + + //TODO:localize + let emptyStateSize = emptyStateView.update( + transition: emptyStateTransition, + component: AnyComponent(EmptyStateIndicatorComponent( + context: self.context, + theme: presentationData.theme, + fitToHeight: self.isProfileEmbedded, + animationName: nil, + title: "Organize Your Stories", + text: "Add some stories to this album.", + actionTitle: "Add to Album", + action: { [weak self] in + guard let self else { + return + } + self.presentAddStoriesToFolder() + }, + additionalActionTitle: nil, + additionalAction: {}, + additionalActionSeparator: nil + )), + environment: {}, + containerSize: CGSize(width: size.width, height: size.height - gridTopInset - bottomInset) + ) + + let emptyStateFrame: CGRect + if self.isProfileEmbedded { + emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: max(gridTopInset + 22.0, floor((visibleHeight - bottomInset - emptyStateSize.height) * 0.5))), size: emptyStateSize) + } else { + emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: gridTopInset), size: emptyStateSize) + } + + if let emptyStateComponentView = emptyStateView.view { + if emptyStateComponentView.superview == nil { + self.view.addSubview(emptyStateComponentView) + if self.didUpdateItemsOnce { + emptyStateComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + emptyStateTransition.setFrame(view: emptyStateComponentView, frame: emptyStateFrame) + } + + let backgroundColor: UIColor + if self.isProfileEmbedded, case .botPreview = self.scope { + backgroundColor = presentationData.theme.list.blocksBackgroundColor + } else if self.isProfileEmbedded { + backgroundColor = presentationData.theme.list.blocksBackgroundColor + } else { + backgroundColor = presentationData.theme.list.blocksBackgroundColor + } + + if self.didUpdateItemsOnce { + ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)).setBackgroundColor(view: self.view, color: backgroundColor) + } else { + self.view.backgroundColor = backgroundColor + } + } else { + let emptyStateView: ComponentView + var emptyStateTransition = ComponentTransition(transition) + if let current = self.emptyStateView { + emptyStateView = current + } else { + emptyStateTransition = .immediate + emptyStateView = ComponentView() + self.emptyStateView = emptyStateView + } + let emptyStateSize = emptyStateView.update( + transition: emptyStateTransition, + component: AnyComponent(EmptyStateIndicatorComponent( + context: self.context, + theme: presentationData.theme, + fitToHeight: self.isProfileEmbedded, + animationName: "StoryListEmpty", + title: isArchived ? presentationData.strings.StoryList_ArchivedEmptyState_Title : presentationData.strings.StoryList_SavedEmptyPosts_Title, + text: isArchived ? presentationData.strings.StoryList_ArchivedEmptyState_Text : presentationData.strings.StoryList_SavedEmptyPosts_Text, + actionTitle: (isArchived || !self.canManageStories) ? nil : presentationData.strings.StoryList_SavedAddAction, + action: { [weak self] in + guard let self else { + return + } + self.emptyAction?() + }, + additionalActionTitle: (isArchived || self.isProfileEmbedded || !self.canManageStories) ? nil : presentationData.strings.StoryList_SavedEmptyAction, + additionalAction: { [weak self] in + guard let self else { + return + } + self.additionalEmptyAction?() + } + )), + environment: {}, + containerSize: CGSize(width: size.width, height: size.height - gridTopInset - bottomInset) + ) + + let emptyStateFrame: CGRect + if self.isProfileEmbedded { + emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: max(gridTopInset, floor((visibleHeight - gridTopInset - bottomInset - emptyStateSize.height) * 0.5))), size: emptyStateSize) + } else { + emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: gridTopInset), size: emptyStateSize) + } + + if let emptyStateComponentView = emptyStateView.view { + if emptyStateComponentView.superview == nil { + self.view.addSubview(emptyStateComponentView) + if self.didUpdateItemsOnce { + emptyStateComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + emptyStateTransition.setFrame(view: emptyStateComponentView, frame: emptyStateFrame) + } + + let backgroundColor: UIColor + if self.isProfileEmbedded { + backgroundColor = presentationData.theme.list.plainBackgroundColor + } else { + backgroundColor = presentationData.theme.list.blocksBackgroundColor + } + + if self.didUpdateItemsOnce { + ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)).setBackgroundColor(view: self.view, color: backgroundColor) + } else { + self.view.backgroundColor = backgroundColor + } + } + } else if case .botPreview = self.scope, let items = self.items, items.items.isEmpty, items.count == 0 { let emptyStateView: ComponentView var emptyStateTransition = ComponentTransition(transition) if let current = self.emptyStateView { @@ -3949,29 +4592,44 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr emptyStateView = ComponentView() self.emptyStateView = emptyStateView } + + var isMainLanguage = true + if let listSource = self.listSource as? BotPreviewStoryListContext, let _ = listSource.language { + isMainLanguage = false + } + let emptyStateSize = emptyStateView.update( transition: emptyStateTransition, component: AnyComponent(EmptyStateIndicatorComponent( context: self.context, theme: presentationData.theme, fitToHeight: self.isProfileEmbedded, - animationName: "StoryListEmpty", - title: isArchived ? presentationData.strings.StoryList_ArchivedEmptyState_Title : presentationData.strings.StoryList_SavedEmptyPosts_Title, - text: isArchived ? presentationData.strings.StoryList_ArchivedEmptyState_Text : presentationData.strings.StoryList_SavedEmptyPosts_Text, - actionTitle: isArchived ? nil : presentationData.strings.StoryList_SavedAddAction, + animationName: nil, + title: presentationData.strings.BotPreviews_Empty_Title, + text: presentationData.strings.BotPreviews_Empty_Text(Int32(self.maxBotPreviewCount)), + actionTitle: self.canManageStories ? presentationData.strings.BotPreviews_Empty_Add : nil, action: { [weak self] in guard let self else { return } - self.emptyAction?() + if self.canAddMoreBotPreviews() { + self.emptyAction?() + } else { + self.presentUnableToAddMorePreviewsAlert() + } }, - additionalActionTitle: (isArchived || self.isProfileEmbedded) ? nil : presentationData.strings.StoryList_SavedEmptyAction, + additionalActionTitle: self.canManageStories ? (isMainLanguage ? presentationData.strings.BotPreviews_Empty_AddTranslation : presentationData.strings.BotPreviews_Empty_DeleteTranslation) : nil, additionalAction: { [weak self] in guard let self else { return } - self.additionalEmptyAction?() - } + if isMainLanguage { + self.presentAddBotPreviewLanguage() + } else { + self.presentDeleteBotPreviewLanguage() + } + }, + additionalActionSeparator: self.canManageStories ? presentationData.strings.BotPreviews_Empty_Separator : nil )), environment: {}, containerSize: CGSize(width: size.width, height: size.height - gridTopInset - bottomInset) @@ -3979,7 +4637,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr let emptyStateFrame: CGRect if self.isProfileEmbedded { - emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: max(gridTopInset, floor((visibleHeight - gridTopInset - bottomInset - emptyStateSize.height) * 0.5))), size: emptyStateSize) + emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: max(gridTopInset + 22.0, floor((visibleHeight - gridTopInset - bottomInset - emptyStateSize.height) * 0.5))), size: emptyStateSize) } else { emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: gridTopInset), size: emptyStateSize) } @@ -3995,8 +4653,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } let backgroundColor: UIColor - if self.isProfileEmbedded { - backgroundColor = presentationData.theme.list.plainBackgroundColor + if self.isProfileEmbedded, case .botPreview = self.scope { + backgroundColor = presentationData.theme.list.blocksBackgroundColor + } else if self.isProfileEmbedded { + backgroundColor = presentationData.theme.list.blocksBackgroundColor } else { backgroundColor = presentationData.theme.list.blocksBackgroundColor } @@ -4006,7 +4666,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } else { self.view.backgroundColor = backgroundColor } - } else if case .botPreview = self.scope, let items = self.items, items.items.isEmpty, items.count == 0 { + } else if case let .peer(peerId, _, isArchived) = self.scope, peerId == self.context.account.peerId, !isArchived, self.isProfileEmbedded, let items = self.items, items.items.isEmpty, items.count == 0 { let emptyStateView: ComponentView var emptyStateTransition = ComponentTransition(transition) if let current = self.emptyStateView { @@ -4104,6 +4764,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if self.isProfileEmbedded, case .botPreview = self.scope { subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.blocksBackgroundColor) + } else if self.isProfileEmbedded, case let .peer(peerId, _, isArchived) = self.scope, ((peerId == self.context.account.peerId && !isArchived) || !self.currentStoryFolders.isEmpty), self.isProfileEmbedded { + subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.blocksBackgroundColor) } else if self.isProfileEmbedded { subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.plainBackgroundColor) } else { @@ -4112,6 +4774,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } else { if self.isProfileEmbedded, case .botPreview = self.scope { self.view.backgroundColor = presentationData.theme.list.blocksBackgroundColor + } else if self.isProfileEmbedded, case let .peer(peerId, _, isArchived) = self.scope, peerId == self.context.account.peerId, self.isProfileEmbedded, !isArchived { + self.view.backgroundColor = presentationData.theme.list.blocksBackgroundColor } else { if case let .search(peerId, _) = self.scope, peerId != nil { @@ -4135,6 +4799,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr var adjustForSmallCount = true if case .botPreview = self.scope { adjustForSmallCount = false + } else if self.currentStoryFolder != nil { + adjustForSmallCount = false } self.itemGrid.pinchEnabled = items.count > 2 && !self.isReordering @@ -4237,10 +4903,21 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } public func canReorder() -> Bool { - guard let items = self.items else { - return false + if case .botPreview = self.scope { + guard let items = self.items else { + return false + } + return items.count > 1 + } else { + if self.currentStoryFolder == nil { + return false + } + + guard let items = self.items else { + return false + } + return items.count > 1 } - return items.count > 1 } private func presentAddBotPreviewLanguage() { @@ -4253,6 +4930,116 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr })) } + private func presentAddStoryFolder(addItems: [EngineStoryItem] = []) { + //TODO:localize + let promptController = promptController( + sharedContext: self.context.sharedContext, + updatedPresentationData: nil, + text: "Create a New Album", + titleFont: .bold, + subtitle: "Choose a name for your album and start adding your stories there.", + value: "", + placeholder: "Title", + characterLimit: 20, + displayCharacterLimit: true, + apply: { [weak self] value in + guard let self else { + return + } + if let value { + if let listSource = self.listSource as? PeerStoryListContext { + let _ = listSource.addFolder(title: value, items: [], completion: { [weak self] id in + Queue.mainQueue().async { + guard let self, let id, let listSource = self.listSource as? PeerStoryListContext else { + return + } + if !addItems.isEmpty { + let _ = listSource.addToFolder(id: id, items: addItems) + } + self.setStoryFolder(id: id, assumeEmpty: addItems.isEmpty) + } + }) + } + } + } + ) + self.parentController?.present(promptController, in: .window(.root)) + } + + private func presentRenameStoryFolder(id: Int64, title: String) { + //TODO:localize + let promptController = promptController( + sharedContext: self.context.sharedContext, + updatedPresentationData: nil, + text: "Edit Album Name", + titleFont: .bold, + subtitle: "Choose a new name for your album.", + value: title, + placeholder: "Title", + characterLimit: 20, + displayCharacterLimit: true, + apply: { [weak self] value in + guard let self else { + return + } + if let value { + if let listSource = self.listSource as? PeerStoryListContext { + let _ = listSource.renameFolder(id: id, title: value) + } + } + } + ) + self.parentController?.present(promptController, in: .window(.root)) + } + + private func presentAddStoriesToFolder() { + guard case let .peer(peerId, _, _) = self.scope else { + return + } + guard let folder = self.currentStoryFolder else { + return + } + + let controller = self.context.sharedContext.makeStorySelectionController(context: self.context, peerId: peerId, completion: { [weak self] items in + guard let self else { + return + } + if let listSource = self.listSource as? PeerStoryListContext { + let _ = listSource.addToFolder(id: folder.id, items: items) + } + }) + controller.navigationPresentation = .modal + + self.parentController?.push(controller) + } + + public func presentDeleteCurrentStoryFolder() { + if let folder = self.currentStoryFolder { + self.presentDeleteStoryFolder(id: folder.id) + } + } + + private func presentDeleteStoryFolder(id: Int64) { + guard let folder = self.currentStoryFolders.first(where: { $0.id == id }) else { + return + } + let _ = folder + + if self.currentStoryFolder?.id == id { + self.setStoryFolder(id: nil, assumeEmpty: false) + } + self.currentStoryFolders.removeAll(where: { $0.id == id }) + self.removedStoryFolders.insert(id) + + if let listContext = self.listSource as? PeerStoryListContext { + let _ = listContext.removeFolder(id: id) + } + + if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams { + self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: false, transition: .immediate) + } + } + public func presentUnableToAddMorePreviewsAlert() { self.parentController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.BotPreviews_AlertTooManyPreviews(Int32(self.maxBotPreviewCount)), actions: [ TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { @@ -4338,12 +5125,12 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } if let id { - if let cachedListSource = self.cachedListSources[id] { + if let cachedListSource = self.cachedListSources[AnyHashable(id)] { self.listSource = cachedListSource } else { let listSource = BotPreviewStoryListContext(account: self.context.account, engine: self.context.engine, peerId: peerId, language: id, assumeEmpty: assumeEmpty) self.listSource = listSource - self.cachedListSources[id] = listSource + self.cachedListSources[AnyHashable(id)] = listSource } } else { self.listSource = self.defaultListSource @@ -4352,6 +5139,26 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.requestHistoryAroundVisiblePosition(synchronous: false, reloadAtTop: true) } + private func setStoryFolder(id: Int64?, assumeEmpty: Bool, animated: Bool = true) { + if let listSource = self.listSource as? PeerStoryListContext, listSource.folderId == id { + return + } + + if let id { + if let cachedListSource = self.cachedListSources[AnyHashable(id)] { + self.listSource = cachedListSource + } else if case let .peer(peerId, _, _) = self.scope { + let listSource = PeerStoryListContext(account: self.context.account, peerId: peerId, isArchived: false, folderId: id) + self.listSource = listSource + //self.cachedListSources[AnyHashable(id)] = listSource + } + } else { + self.listSource = self.defaultListSource + } + + self.requestHistoryAroundVisiblePosition(synchronous: false, reloadAtTop: true, animated: animated) + } + public func beginReordering() { if let parentController = self.parentController as? PeerInfoScreen { parentController.togglePaneIsReordering(isReordering: true) @@ -4376,6 +5183,12 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.itemGrid.setReordering(isReordering: isReordering) + if !isReordering && !self.currentStoryFolders.isEmpty && self.canManageStories { + if let listSource = self.listSource as? PeerStoryListContext { + let _ = listSource.reorderFolders(ids: self.currentStoryFolders.map(\.id)) + } + } + if !isReordering, let reorderedIds = self.reorderedIds { self.reorderedIds = nil if case .botPreview = self.scope, let listSource = self.listSource as? BotPreviewStoryListContext { @@ -4391,26 +5204,84 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr listSource.reorderItems(media: reorderedMedia) } } else if case let .peer(id, _, _) = self.scope, id == self.context.account.peerId, let items = self.items { - var updatedPinnedIds: [Int32] = [] - for id in reorderedIds { - inner: for item in items.items { - if let item = item as? VisualMediaItem { - if item.storyId == id { - if item.isPinned { - updatedPinnedIds.append(id.id) - break inner + if let _ = self.currentStoryFolder { + if let listSource = self.listSource as? PeerStoryListContext { + let _ = listSource.reorderItemsInFolder(itemIds: reorderedIds.map { $0.id }) + } + } else { + var updatedPinnedIds: [Int32] = [] + for id in reorderedIds { + inner: for item in items.items { + if let item = item as? VisualMediaItem { + if item.storyId == id { + if item.isPinned { + updatedPinnedIds.append(id.id) + break inner + } } } } } + let _ = self.context.engine.messages.updatePinnedToTopStories(peerId: id, ids: updatedPinnedIds).startStandalone() } - let _ = self.context.engine.messages.updatePinnedToTopStories(peerId: id, ids: updatedPinnedIds).startStandalone() } } self.update(transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate) } } + + public func shareCurrentFolder() { + guard let folder = self.currentStoryFolder else { + return + } + self.shareFolder(id: folder.id) + } + + private func shareFolder(id: Int64) { + guard case let .peer(peerId, _, _) = self.scope else { + return + } + + Task { @MainActor [weak self] in + guard let self else { + return + } + + guard let peer = await self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + ).get() else { + return + } + + let urlBase: String + if let addressName = peer.addressName { + urlBase = "\(addressName)" + } else { + urlBase = "\(peer.id.id._internalGetInt64Value())" + } + + let shareController = ShareController( + context: self.context, + subject: .url("https://t.me/\(urlBase)?stories=\(id)"), + presetText: nil, + preferredAction: .default, + showInChat: nil, + fromForeignApp: false, + segmentedValues: nil, + externalShare: false, + immediateExternalShare: false, + switchableAccounts: [], + immediatePeerId: nil, + updatedPresentationData: nil, + forceTheme: nil, + forcedActionTitle: nil, + shareAsLink: false, + collectibleItemInfo: nil + ) + self.parentController?.present(shareController, in: .window(.root)) + } + } } private class MediaListSelectionRecognizer: UIPanGestureRecognizer { @@ -4762,3 +5633,34 @@ private final class BottomActionsPanelComponent: Component { } } +private final class ItemExtractedContentSource: ContextExtractedContentSource { + let keepInPlace: Bool + let ignoreContentTouches: Bool = true + let blurBackground: Bool = true + let adjustContentForSideInset: Bool = true + + private let sourceNode: ContextExtractedContentContainingNode + private weak var containerView: UIView? + + init(sourceNode: ContextExtractedContentContainingNode, containerView: UIView, keepInPlace: Bool) { + self.sourceNode = sourceNode + self.containerView = containerView + self.keepInPlace = keepInPlace + } + + func takeView() -> ContextControllerTakeViewInfo? { + var contentArea: CGRect? + if let containerView = self.containerView { + contentArea = containerView.convert(containerView.bounds, to: nil) + } + + return ContextControllerTakeViewInfo( + containingItem: .node(self.sourceNode), + contentAreaInScreenSpace: contentArea ?? UIScreen.main.bounds + ) + } + + func putBack() -> ContextControllerPutBackViewInfo? { + return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) + } +} diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index cf04937b71..afc34227b2 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -44,9 +44,9 @@ final class ShareWithPeersScreenComponent: Component { let categoryItems: [CategoryItem] let optionItems: [OptionItem] let coverItem: CoverItem? - let completion: (EnginePeer.Id?, EngineStoryPrivacy, Bool, Bool, [EnginePeer], Bool) -> Void - let editCategory: (EngineStoryPrivacy, Bool, Bool) -> Void - let editBlockedPeers: (EngineStoryPrivacy, Bool, Bool) -> Void + let completion: (EnginePeer.Id?, EngineStoryPrivacy, Bool, Bool, [EnginePeer], [Int64], Bool) -> Void + let editCategory: (EngineStoryPrivacy, Bool, Bool, [Int64]) -> Void + let editBlockedPeers: (EngineStoryPrivacy, Bool, Bool, [Int64]) -> Void let editCover: () -> Void let peerCompletion: (EnginePeer.Id) -> Void @@ -62,9 +62,9 @@ final class ShareWithPeersScreenComponent: Component { categoryItems: [CategoryItem], optionItems: [OptionItem], coverItem: CoverItem?, - completion: @escaping (EnginePeer.Id?, EngineStoryPrivacy, Bool, Bool, [EnginePeer], Bool) -> Void, - editCategory: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void, - editBlockedPeers: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void, + completion: @escaping (EnginePeer.Id?, EngineStoryPrivacy, Bool, Bool, [EnginePeer], [Int64], Bool) -> Void, + editCategory: @escaping (EngineStoryPrivacy, Bool, Bool, [Int64]) -> Void, + editBlockedPeers: @escaping (EngineStoryPrivacy, Bool, Bool, [Int64]) -> Void, editCover: @escaping () -> Void, peerCompletion: @escaping (EnginePeer.Id) -> Void ) { @@ -833,9 +833,9 @@ final class ShareWithPeersScreenComponent: Component { context: component.context, initialPrivacy: EngineStoryPrivacy(base: .nobody, additionallyIncludePeers: []), stateContext: stateContext, - completion: { _, _, _, _, _, _ in }, - editCategory: { _, _, _ in }, - editBlockedPeers: { _, _, _ in }, + completion: { _, _, _, _, _, _, _ in }, + editCategory: { _, _, _, _ in }, + editBlockedPeers: { _, _, _, _ in }, peerCompletion: { [weak self] peerId in guard let self else { return @@ -896,7 +896,7 @@ final class ShareWithPeersScreenComponent: Component { var items: [ContextMenuItem] = [] - items.append(.action(ContextMenuActionItem(text: "New Album", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Folder"), color: theme.contextMenu.primaryColor) }, iconPosition: .left, action: { [weak self] c, f in + items.append(.action(ContextMenuActionItem(text: "New Album", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddFolder"), color: theme.contextMenu.primaryColor) }, iconPosition: .left, action: { [weak self] c, f in guard let self else { f(.default) return @@ -1005,12 +1005,17 @@ final class ShareWithPeersScreenComponent: Component { return } if let value, !value.isEmpty { - let id = PeerStoryListContext.addFolderExternal(account: component.context.account, peerId: component.context.account.peerId, title: value) - self.shareToFolders.append(StoryListContext.State.Folder( - id: id, - title: value - )) - self.state?.updated(transition: .immediate) + let _ = PeerStoryListContext.addFolderExternal(account: component.context.account, peerId: component.context.account.peerId, title: value, completion: { [weak self] id in + guard let self, let id else { + return + } + + self.shareToFolders.append(StoryListContext.State.Folder( + id: id, + title: value + )) + self.state?.updated(transition: .immediate) + }) } } ) @@ -1416,7 +1421,8 @@ final class ShareWithPeersScreenComponent: Component { component.editCategory( EngineStoryPrivacy(base: .nobody, additionallyIncludePeers: []), self.selectedOptions.contains(.screenshot), - self.selectedOptions.contains(.pin) + self.selectedOptions.contains(.pin), + self.shareToFolders.map(\.id) ) controller.dismissAllTooltips() controller.dismiss() @@ -1424,7 +1430,8 @@ final class ShareWithPeersScreenComponent: Component { component.editCategory( EngineStoryPrivacy(base: .closeFriends, additionallyIncludePeers: []), self.selectedOptions.contains(.screenshot), - self.selectedOptions.contains(.pin) + self.selectedOptions.contains(.pin), + self.shareToFolders.map(\.id) ) controller.dismissAllTooltips() controller.dismiss() @@ -1452,7 +1459,8 @@ final class ShareWithPeersScreenComponent: Component { component.editCategory( EngineStoryPrivacy(base: base, additionallyIncludePeers: selectedPeers), self.selectedOptions.contains(.screenshot), - self.selectedOptions.contains(.pin) + self.selectedOptions.contains(.pin), + self.shareToFolders.map(\.id) ) controller.dismissAllTooltips() controller.dismiss() @@ -1540,7 +1548,8 @@ final class ShareWithPeersScreenComponent: Component { component.editBlockedPeers( EngineStoryPrivacy(base: base, additionallyIncludePeers: self.selectedPeers), self.selectedOptions.contains(.screenshot), - self.selectedOptions.contains(.pin) + self.selectedOptions.contains(.pin), + self.shareToFolders.map(\.id) ) controller.dismissAllTooltips() controller.dismiss() @@ -2262,6 +2271,7 @@ final class ShareWithPeersScreenComponent: Component { self.selectedOptions.contains(.screenshot), self.selectedOptions.contains(.pin), self.component?.stateContext.stateValue?.peers.filter { self.selectedPeers.contains($0.id) } ?? [], + self.shareToFolders.map(\.id), false ) controller.requestDismiss() @@ -3014,6 +3024,7 @@ final class ShareWithPeersScreenComponent: Component { self.selectedOptions.contains(.screenshot), self.selectedOptions.contains(.pin), peers.values.compactMap { $0 }, + self.shareToFolders.map(\.id), true ) @@ -3265,9 +3276,9 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { mentions: [String] = [], coverImage: UIImage? = nil, stateContext: StateContext, - completion: @escaping (EnginePeer.Id?, EngineStoryPrivacy, Bool, Bool, [EnginePeer], Bool) -> Void, - editCategory: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void = { _, _, _ in }, - editBlockedPeers: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void = { _, _, _ in }, + completion: @escaping (EnginePeer.Id?, EngineStoryPrivacy, Bool, Bool, [EnginePeer], [Int64], Bool) -> Void, + editCategory: @escaping (EngineStoryPrivacy, Bool, Bool, [Int64]) -> Void = { _, _, _, _ in }, + editBlockedPeers: @escaping (EngineStoryPrivacy, Bool, Bool, [Int64]) -> Void = { _, _, _, _ in }, editCover: @escaping () -> Void = { }, peerCompletion: @escaping (EnginePeer.Id) -> Void = { _ in } ) { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index 5c5eddbd06..f3b505feae 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -318,7 +318,8 @@ public final class StoryContentContextImpl: StoryContentContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: forwardInfo, - author: item.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) } + author: item.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) }, + folderIds: item.folderIds ) } var totalCount = peerStoryItemsView.items.count @@ -355,7 +356,8 @@ public final class StoryContentContextImpl: StoryContentContext { isMy: true, myReaction: nil, forwardInfo: pendingForwardsInfo[item.randomId], - author: nil + author: nil, + folderIds: item.folders )) totalCount += 1 } @@ -1354,7 +1356,8 @@ public final class SingleStoryContentContextImpl: StoryContentContext { isMy: itemValue.isMy, myReaction: itemValue.myReaction, forwardInfo: forwardInfo, - author: itemValue.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) } + author: itemValue.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) }, + folderIds: itemValue.folderIds ) let mainItem = StoryContentItem( @@ -2292,7 +2295,8 @@ private func getCachedStory(storyId: StoryId, transaction: Transaction) -> Engin isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }, - author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) } + author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) }, + folderIds: item.folderIds ) } else { return nil diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index b22b6aefb3..1b7327eb71 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -1695,13 +1695,13 @@ private final class StoryContainerScreenComponent: Component { if let content = component.content as? PeerStoryListContentContextImpl { if let listSource = content.listContext as? PeerStoryListContext { - listSource.addFolder(title: title, completion: { [weak listSource] id in + let _ = listSource.addFolder(title: title, items: [], completion: { [weak listSource] id in Queue.mainQueue().async { - guard let listSource else { + guard let listSource, let id else { return } if !items.isEmpty { - listSource.addToFolder(id: id, items: items) + let _ = listSource.addToFolder(id: id, items: items) } } }) @@ -1717,7 +1717,7 @@ private final class StoryContainerScreenComponent: Component { } if let content = component.content as? PeerStoryListContentContextImpl { if let listSource = content.listContext as? PeerStoryListContext { - listSource.addToFolder(id: folderId, items: [slice.item.storyItem]) + let _ = listSource.addToFolder(id: folderId, items: [slice.item.storyItem]) } } }, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 075a35744c..925cca45db 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -5090,7 +5090,7 @@ public final class StoryItemSetContainerComponent: Component { context: context, initialPrivacy: privacy, stateContext: stateContext, - completion: { [weak self] _, privacy, _, _, _, completed in + completion: { [weak self] _, privacy, _, _, _, _, completed in guard let self, let component = self.component, completed else { return } @@ -5107,7 +5107,7 @@ public final class StoryItemSetContainerComponent: Component { self.rewindCurrentItem() self.updateIsProgressPaused() }, - editCategory: { [weak self] privacy, _, _ in + editCategory: { [weak self] privacy, _, _, _ in guard let self else { return } @@ -5118,7 +5118,7 @@ public final class StoryItemSetContainerComponent: Component { self.openItemPrivacySettings(updatedPrivacy: privacy) }) }, - editBlockedPeers: { [weak self] privacy, _, _ in + editBlockedPeers: { [weak self] privacy, _, _, _ in guard let self else { return } @@ -5171,7 +5171,7 @@ public final class StoryItemSetContainerComponent: Component { context: context, initialPrivacy: privacy, stateContext: stateContext, - completion: { [weak self] _, result, _, _, peers, completed in + completion: { [weak self] _, result, _, _, peers, _, completed in guard completed else { return } @@ -5188,8 +5188,8 @@ public final class StoryItemSetContainerComponent: Component { completion(result) } }, - editCategory: { _, _, _ in }, - editBlockedPeers: { _, _, _ in } + editCategory: { _, _, _, _ in }, + editBlockedPeers: { _, _, _, _ in } ) controller.dismissed = { [weak self] in if let self { @@ -6110,7 +6110,7 @@ public final class StoryItemSetContainerComponent: Component { var items: [ContextMenuItem] = [] //TODO:localize - items.append(.action(ContextMenuActionItem(text: "Add to Album", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Folder"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in + items.append(.action(ContextMenuActionItem(text: "Add to Album", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddToFolder"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in guard let self, let c else { f(.default) return @@ -6131,7 +6131,7 @@ public final class StoryItemSetContainerComponent: Component { }))) items.append(.separator) - items.append(.action(ContextMenuActionItem(text: "New Album", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Folder"), color: theme.contextMenu.primaryColor) }, iconPosition: .left, action: { [weak self] c, f in + items.append(.action(ContextMenuActionItem(text: "New Album", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddFolder"), color: theme.contextMenu.primaryColor) }, iconPosition: .left, action: { [weak self] c, f in guard let self else { f(.default) return diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddFolder.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddFolder.imageset/Contents.json new file mode 100644 index 0000000000..7788d5e229 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddFolder.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "addfolder_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddFolder.imageset/addfolder_24.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddFolder.imageset/addfolder_24.pdf new file mode 100644 index 0000000000..e5434739fd Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddFolder.imageset/addfolder_24.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddToFolder.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddToFolder.imageset/Contents.json new file mode 100644 index 0000000000..c4e09e2102 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddToFolder.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "folder1_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddToFolder.imageset/folder1_24.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddToFolder.imageset/folder1_24.pdf new file mode 100644 index 0000000000..e941baf3a6 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddToFolder.imageset/folder1_24.pdf differ diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index f24d0624f1..2f9e8b5979 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -1498,5 +1498,26 @@ func openResolvedUrlImpl( } else { present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.BusinessLink_ErrorExpired, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) } + case let .storyFolder(peerId, id): + Task { @MainActor [weak navigationController] in + guard let peer = await context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + ).get() else { + return + } + + guard let controller = context.sharedContext.makePeerInfoController( + context: context, + updatedPresentationData: updatedPresentationData, + peer: peer._asPeer(), + mode: .storyAlbum(id: id), + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) else { + return + } + navigationController?.pushViewController(controller) + } } } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index dcb2d5e440..9f4b2f0fe6 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -3964,6 +3964,7 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation var isMyProfile = false var switchToGifts = false var switchToGroupsInCommon = false + var switchToStoryFolder: Int64? switch mode { case let .nearbyPeer(distance): @@ -3989,10 +3990,12 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation switchToGroupsInCommon = true case let .monoforum(peerId): sharedMediaFromForumTopic = (peerId, peer.id.toInt64()) + case let .storyAlbum(id): + switchToStoryFolder = id default: break } - return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nearbyPeerDistance, reactionSourceMessageId: reactionSourceMessageId, callMessages: callMessages, isMyProfile: isMyProfile, hintGroupInCommon: hintGroupInCommon, forumTopicThread: forumTopicThread, sharedMediaFromForumTopic: sharedMediaFromForumTopic, switchToGifts: switchToGifts, switchToGroupsInCommon: switchToGroupsInCommon) + return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nearbyPeerDistance, reactionSourceMessageId: reactionSourceMessageId, callMessages: callMessages, isMyProfile: isMyProfile, hintGroupInCommon: hintGroupInCommon, forumTopicThread: forumTopicThread, sharedMediaFromForumTopic: sharedMediaFromForumTopic, switchToGifts: switchToGifts, switchToGroupsInCommon: switchToGroupsInCommon, switchToStoryFolder: switchToStoryFolder) } else if peer is TelegramSecretChat { return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: []) } diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 35348db5b1..8d062984b6 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -487,7 +487,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon externalState.isPeerArchived = channel.storiesHidden ?? false } - self.proceedWithStoryUpload(target: target, results: results, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) + self.proceedWithStoryUpload(target: target, results: results, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) dismissCameraImpl?() }) @@ -565,6 +565,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon case .botPreview: targetPeerId = nil } + + let folders: [Int64] = results.first?.options.folderIds ?? [] if let rootTabController = self.rootTabController { if let index = rootTabController.controllers.firstIndex(where: { $0 is ChatListController}) { @@ -736,6 +738,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon period: result.options.timeout, randomId: result.randomId, forwardInfo: forwardInfo, + folders: folders, uploadInfo: results.count > 1 ? StoryUploadInfo(groupingId: groupingId, index: index, total: Int32(results.count)) : nil ) |> deliverOnMainQueue).startStandalone(next: { stableId in diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index f6687eb20d..7123e227f2 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -81,6 +81,7 @@ public enum ParsedInternalPeerUrlParameter { case text(String) case profile case referrer(String) + case storyFolder(Int64) } public enum ParsedInternalUrl { @@ -345,6 +346,8 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, context: Accou } } else if queryItem.name == "ref", let referrer = queryItem.value { return .peer(.name(peerName), .referrer(referrer)) + } else if queryItem.name == "stories", let value = queryItem.value, let folderId = Int64(value) { + return .peer(.name(peerName), .storyFolder(folderId)) } } else if ["voicechat", "videochat", "livestream"].contains(queryItem.name) { return .peer(.name(peerName), .voiceChat(nil)) @@ -401,6 +404,8 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, context: Accou return .peer(.name(peerName), .appStart("", queryItem.value, mode)) } else if queryItem.name == "ref", let referrer = queryItem.value { return .peer(.name(peerName), .referrer(referrer)) + } else if queryItem.name == "stories", let value = queryItem.value, let folderId = Int64(value) { + return .peer(.name(peerName), .storyFolder(folderId)) } } } @@ -939,6 +944,8 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) ) case .referrer: return .single(.result(.peer(peer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil)))) + case let .storyFolder(folderId): + return .single(.result(.storyFolder(peerId: peer.id, id: folderId))) } } else { return .single(.result(.peer(peer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil)))) diff --git a/versions.json b/versions.json index 4ff99ef456..473ed33fe9 100644 --- a/versions.json +++ b/versions.json @@ -1,6 +1,6 @@ { "app": "11.13.4", "xcode": "16.2", - "bazel": "8.2.1:22ff65b05869f6160e5157b1b425a14a62085d71d8baef571f462b8fe5a703a3", + "bazel": "8.3.1:0cac3a67dc5429c68272dc6944104952e9e4cf84b29d126a5ff3fbaa59045217", "macos": "15" }