From 72270d288ed390d2fa7be0fe7182eb6e3fb2cd0d Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Sun, 20 Jul 2025 18:50:16 +0200 Subject: [PATCH 01/11] [Temp] fix local build --- MODULE.bazel | 2 +- MODULE.bazel.lock | 39 +++++++++--------------------- Telegram/BUILD | 1 - build-system/Make/BazelLocation.py | 8 +++--- versions.json | 2 +- 5 files changed, 18 insertions(+), 34 deletions(-) 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 9cf530b698..05c81d511c 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -10,8 +10,6 @@ "https://bcr.bazel.build/modules/abseil-cpp/20230802.1/MODULE.bazel": "fa92e2eb41a04df73cdabeec37107316f7e5272650f81d6cc096418fe647b915", "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed", "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/source.json": "9be551b8d4e3ef76875c0d744b5d6a504a27e3ae67bc6b28f46415fd2d2957da", - "https://bcr.bazel.build/modules/aexml/4.7.0/MODULE.bazel": "4030ff1555ade0956c08c74722851fcca0dc02ec7b8e7c61d0bbc4806ec4b2de", - "https://bcr.bazel.build/modules/aexml/4.7.0/source.json": "641c9de95dc10b8bf3685b5de9f9a84e7470ec3e40a09d7ddc9e3c9f2289a931", "https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd", "https://bcr.bazel.build/modules/bazel_features/1.10.0/MODULE.bazel": "f75e8807570484a99be90abcd52b5e1f390362c258bcb73106f4544957a48101", "https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8", @@ -20,9 +18,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", @@ -50,8 +48,6 @@ "https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902", "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/MODULE.bazel": "6f7b417dcc794d9add9e556673ad25cb3ba835224290f4f848f8e2db1e1fca74", "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/source.json": "f448c6e8963fdfa7eb831457df83ad63d3d6355018f6574fb017e8169deb43a9", - "https://bcr.bazel.build/modules/pathkit/1.0.1/MODULE.bazel": "fae93989a10f8d90d5ac02453e6632ae7f71111687862c01f468858cef40bb5e", - "https://bcr.bazel.build/modules/pathkit/1.0.1/source.json": "3215e6b4b08f96f34024eaf186d247744ca255925d7ee3f50cf94f7cf885696b", "https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5", "https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f", "https://bcr.bazel.build/modules/platforms/0.0.11/source.json": "f7e188b79ebedebfe75e9e1d098b8845226c7992b307e28e1496f23112e8fc29", @@ -81,12 +77,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 +96,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", @@ -148,18 +144,16 @@ "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91", "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/source.json": "32bd87e5f4d7acc57c5b2ff7c325ae3061d5e242c0c4c214ae87e0f1c13e54cb", "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", - "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": "Ync9nL0AbHC6ondeEY7fBjBjLxojTsiXcJh65ZDTRlA=", + "bzlTransitiveDigest": "RjubjYIojbv0PxTpnoknalV9QzT9asbV7elDuN7m2A4=", "usagesDigest": "lfcV4HxPD+NLaRIT/v7BtSGFgE7c9xrWU7jDiwBAxzo=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -190,7 +184,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 +284,7 @@ }, "@@rules_xcodeproj+//xcodeproj:extensions.bzl%internal": { "general": { - "bzlTransitiveDigest": "+kmqZtEKFY8zgqpV6mrwdQkTJqGUZhL8b3ZMsxrqSyc=", + "bzlTransitiveDigest": "/r7IWMTdpceHqqiSh7G9bQLHvIkfqE/cesswbkTSJZw=", "usagesDigest": "fvsnMonVwKDYnBfww4bXuYie3WU0d9VSqT2gePSdQco=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -312,7 +306,7 @@ }, "@@rules_xcodeproj+//xcodeproj:extensions.bzl%non_module_deps": { "general": { - "bzlTransitiveDigest": "+kmqZtEKFY8zgqpV6mrwdQkTJqGUZhL8b3ZMsxrqSyc=", + "bzlTransitiveDigest": "/r7IWMTdpceHqqiSh7G9bQLHvIkfqE/cesswbkTSJZw=", "usagesDigest": "jzxYhnOC9BE0dJ0biFLfxWXi/+R19uAAZkJ0p9CY0JI=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -345,15 +339,6 @@ "url": "https://github.com/apple/swift-argument-parser/archive/refs/tags/1.2.3.tar.gz" } }, - "com_github_tadija_aexml": { - "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", - "attributes": { - "build_file_content": "load(\"@build_bazel_rules_swift//swift:swift.bzl\", \"swift_library\")\n\nswift_library(\n name = \"AEXML\",\n srcs = glob([\"Sources/AEXML/**/*.swift\"]),\n visibility = [\"//visibility:public\"],\n)\n", - "sha256": "5a76c28e4fa9dcc1cbfb87a8518652628e990e522ecfbc98bdad17eabf4631d5", - "strip_prefix": "AEXML-4.6.1", - "url": "https://github.com/tadija/AEXML/archive/refs/tags/4.6.1.tar.gz" - } - }, "com_github_michaeleisel_jjliso8601dateformatter": { "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", "attributes": { diff --git a/Telegram/BUILD b/Telegram/BUILD index fb07598b4e..af18262d33 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -1736,7 +1736,6 @@ ios_application( xcodeproj( name = "Telegram_xcodeproj", - build_mode = "bazel", bazel_path = telegram_bazel_path, project_name = "Telegram", tags = ["manual"], 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/versions.json b/versions.json index 4f5268a342..62f1abf20e 100644 --- a/versions.json +++ b/versions.json @@ -1,6 +1,6 @@ { "app": "11.13.2", "xcode": "16.2", - "bazel": "8.2.1:22ff65b05869f6160e5157b1b425a14a62085d71d8baef571f462b8fe5a703a3", + "bazel": "8.3.1:0cac3a67dc5429c68272dc6944104952e9e4cf84b29d126a5ff3fbaa59045217", "macos": "15" } From 0817e0a94381d8bd60c0f0a1e6a4ece814300b65 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Sun, 20 Jul 2025 18:51:35 +0200 Subject: [PATCH 02/11] Update module.bazel.lock --- MODULE.bazel.lock | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 05c81d511c..b361f3adef 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -10,6 +10,8 @@ "https://bcr.bazel.build/modules/abseil-cpp/20230802.1/MODULE.bazel": "fa92e2eb41a04df73cdabeec37107316f7e5272650f81d6cc096418fe647b915", "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed", "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/source.json": "9be551b8d4e3ef76875c0d744b5d6a504a27e3ae67bc6b28f46415fd2d2957da", + "https://bcr.bazel.build/modules/aexml/4.7.0/MODULE.bazel": "4030ff1555ade0956c08c74722851fcca0dc02ec7b8e7c61d0bbc4806ec4b2de", + "https://bcr.bazel.build/modules/aexml/4.7.0/source.json": "641c9de95dc10b8bf3685b5de9f9a84e7470ec3e40a09d7ddc9e3c9f2289a931", "https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd", "https://bcr.bazel.build/modules/bazel_features/1.10.0/MODULE.bazel": "f75e8807570484a99be90abcd52b5e1f390362c258bcb73106f4544957a48101", "https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8", @@ -48,6 +50,8 @@ "https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902", "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/MODULE.bazel": "6f7b417dcc794d9add9e556673ad25cb3ba835224290f4f848f8e2db1e1fca74", "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/source.json": "f448c6e8963fdfa7eb831457df83ad63d3d6355018f6574fb017e8169deb43a9", + "https://bcr.bazel.build/modules/pathkit/1.0.1/MODULE.bazel": "fae93989a10f8d90d5ac02453e6632ae7f71111687862c01f468858cef40bb5e", + "https://bcr.bazel.build/modules/pathkit/1.0.1/source.json": "3215e6b4b08f96f34024eaf186d247744ca255925d7ee3f50cf94f7cf885696b", "https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5", "https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f", "https://bcr.bazel.build/modules/platforms/0.0.11/source.json": "f7e188b79ebedebfe75e9e1d098b8845226c7992b307e28e1496f23112e8fc29", @@ -144,6 +148,8 @@ "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91", "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/source.json": "32bd87e5f4d7acc57c5b2ff7c325ae3061d5e242c0c4c214ae87e0f1c13e54cb", "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", + "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.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca", "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/source.json": "22bc55c47af97246cfc093d0acf683a7869377de362b5d1c552c2c2e16b7a806", @@ -284,7 +290,7 @@ }, "@@rules_xcodeproj+//xcodeproj:extensions.bzl%internal": { "general": { - "bzlTransitiveDigest": "/r7IWMTdpceHqqiSh7G9bQLHvIkfqE/cesswbkTSJZw=", + "bzlTransitiveDigest": "6MYik+6MZUO7rOzaI0dUJYVD8dJrR1Q2rT+5vo1j73U=", "usagesDigest": "fvsnMonVwKDYnBfww4bXuYie3WU0d9VSqT2gePSdQco=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, From 32f0f1a07fafbe693220db8c2075200bb20e19ca Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Mon, 21 Jul 2025 22:27:19 +0200 Subject: [PATCH 03/11] Temp --- MODULE.bazel.lock | 19 ++----------------- Telegram/BUILD | 1 - 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index cf536336c5..169cf0ff99 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -10,8 +10,6 @@ "https://bcr.bazel.build/modules/abseil-cpp/20230802.1/MODULE.bazel": "fa92e2eb41a04df73cdabeec37107316f7e5272650f81d6cc096418fe647b915", "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed", "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/source.json": "9be551b8d4e3ef76875c0d744b5d6a504a27e3ae67bc6b28f46415fd2d2957da", - "https://bcr.bazel.build/modules/aexml/4.7.0/MODULE.bazel": "4030ff1555ade0956c08c74722851fcca0dc02ec7b8e7c61d0bbc4806ec4b2de", - "https://bcr.bazel.build/modules/aexml/4.7.0/source.json": "641c9de95dc10b8bf3685b5de9f9a84e7470ec3e40a09d7ddc9e3c9f2289a931", "https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd", "https://bcr.bazel.build/modules/bazel_features/1.10.0/MODULE.bazel": "f75e8807570484a99be90abcd52b5e1f390362c258bcb73106f4544957a48101", "https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8", @@ -50,8 +48,6 @@ "https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902", "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/MODULE.bazel": "6f7b417dcc794d9add9e556673ad25cb3ba835224290f4f848f8e2db1e1fca74", "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/source.json": "f448c6e8963fdfa7eb831457df83ad63d3d6355018f6574fb017e8169deb43a9", - "https://bcr.bazel.build/modules/pathkit/1.0.1/MODULE.bazel": "fae93989a10f8d90d5ac02453e6632ae7f71111687862c01f468858cef40bb5e", - "https://bcr.bazel.build/modules/pathkit/1.0.1/source.json": "3215e6b4b08f96f34024eaf186d247744ca255925d7ee3f50cf94f7cf885696b", "https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5", "https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f", "https://bcr.bazel.build/modules/platforms/0.0.11/source.json": "f7e188b79ebedebfe75e9e1d098b8845226c7992b307e28e1496f23112e8fc29", @@ -148,8 +144,6 @@ "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91", "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/source.json": "32bd87e5f4d7acc57c5b2ff7c325ae3061d5e242c0c4c214ae87e0f1c13e54cb", "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", - "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", @@ -290,7 +284,7 @@ }, "@@rules_xcodeproj+//xcodeproj:extensions.bzl%internal": { "general": { - "bzlTransitiveDigest": "+kmqZtEKFY8zgqpV6mrwdQkTJqGUZhL8b3ZMsxrqSyc=", + "bzlTransitiveDigest": "m7EcAC1RuIDErOmw2fxoQ7OGzFPZJtVcXgIIqZMO3OI=", "usagesDigest": "fvsnMonVwKDYnBfww4bXuYie3WU0d9VSqT2gePSdQco=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -312,7 +306,7 @@ }, "@@rules_xcodeproj+//xcodeproj:extensions.bzl%non_module_deps": { "general": { - "bzlTransitiveDigest": "+kmqZtEKFY8zgqpV6mrwdQkTJqGUZhL8b3ZMsxrqSyc=", + "bzlTransitiveDigest": "m7EcAC1RuIDErOmw2fxoQ7OGzFPZJtVcXgIIqZMO3OI=", "usagesDigest": "jzxYhnOC9BE0dJ0biFLfxWXi/+R19uAAZkJ0p9CY0JI=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -345,15 +339,6 @@ "url": "https://github.com/apple/swift-argument-parser/archive/refs/tags/1.2.3.tar.gz" } }, - "com_github_tadija_aexml": { - "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", - "attributes": { - "build_file_content": "load(\"@build_bazel_rules_swift//swift:swift.bzl\", \"swift_library\")\n\nswift_library(\n name = \"AEXML\",\n srcs = glob([\"Sources/AEXML/**/*.swift\"]),\n visibility = [\"//visibility:public\"],\n)\n", - "sha256": "5a76c28e4fa9dcc1cbfb87a8518652628e990e522ecfbc98bdad17eabf4631d5", - "strip_prefix": "AEXML-4.6.1", - "url": "https://github.com/tadija/AEXML/archive/refs/tags/4.6.1.tar.gz" - } - }, "com_github_michaeleisel_jjliso8601dateformatter": { "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", "attributes": { diff --git a/Telegram/BUILD b/Telegram/BUILD index fb07598b4e..af18262d33 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -1736,7 +1736,6 @@ ios_application( xcodeproj( name = "Telegram_xcodeproj", - build_mode = "bazel", bazel_path = telegram_bazel_path, project_name = "Telegram", tags = ["manual"], From cf3dbeee29ab89484d8714fe478cf90aeb18e9f1 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 22 Jul 2025 00:13:32 +0200 Subject: [PATCH 04/11] Update --- MODULE.bazel.lock | 11 +- .../Sources/AccountContext.swift | 2 +- submodules/TelegramApi/Sources/Api0.swift | 9 +- submodules/TelegramApi/Sources/Api26.swift | 80 ++- submodules/TelegramApi/Sources/Api37.swift | 110 ++-- submodules/TelegramApi/Sources/Api38.swift | 52 ++ submodules/TelegramApi/Sources/Api39.swift | 135 ++++- .../Sources/State/Serialization.swift | 2 +- .../Messages/PendingStoryManager.swift | 12 +- .../TelegramEngine/Messages/Stories.swift | 22 +- .../Messages/StoryListContext.swift | 491 +++++++++++------- .../Messages/TelegramEngineMessages.swift | 4 +- .../Sources/EditStories.swift | 2 +- .../Sources/PeerInfoScreen.swift | 2 +- .../Sources/PeerInfoStoryPaneNode.swift | 18 +- .../Sources/ShareWithPeersScreen.swift | 17 +- .../Sources/StoryContainerScreen.swift | 8 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 2 +- .../Sources/SharedAccountContext.swift | 2 +- .../Sources/TelegramRootController.swift | 7 +- .../WebUI/Sources/WebAppController.swift | 2 +- 21 files changed, 703 insertions(+), 287 deletions(-) diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index b361f3adef..ed86535e8b 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -312,7 +312,7 @@ }, "@@rules_xcodeproj+//xcodeproj:extensions.bzl%non_module_deps": { "general": { - "bzlTransitiveDigest": "/r7IWMTdpceHqqiSh7G9bQLHvIkfqE/cesswbkTSJZw=", + "bzlTransitiveDigest": "6MYik+6MZUO7rOzaI0dUJYVD8dJrR1Q2rT+5vo1j73U=", "usagesDigest": "jzxYhnOC9BE0dJ0biFLfxWXi/+R19uAAZkJ0p9CY0JI=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -345,6 +345,15 @@ "url": "https://github.com/apple/swift-argument-parser/archive/refs/tags/1.2.3.tar.gz" } }, + "com_github_tadija_aexml": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "build_file_content": "load(\"@build_bazel_rules_swift//swift:swift.bzl\", \"swift_library\")\n\nswift_library(\n name = \"AEXML\",\n srcs = glob([\"Sources/AEXML/**/*.swift\"]),\n visibility = [\"//visibility:public\"],\n)\n", + "sha256": "5a76c28e4fa9dcc1cbfb87a8518652628e990e522ecfbc98bdad17eabf4631d5", + "strip_prefix": "AEXML-4.6.1", + "url": "https://github.com/tadija/AEXML/archive/refs/tags/4.6.1.tar.gz" + } + }, "com_github_michaeleisel_jjliso8601dateformatter": { "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", "attributes": { diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 7b0b0d692f..fde006cf19 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -884,7 +884,7 @@ public protocol MediaEditorScreenResult { public protocol TelegramRootControllerInterface: NavigationController { @discardableResult func openStoryCamera(customTarget: Stories.PendingTarget?, transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? - func proceedWithStoryUpload(target: Stories.PendingTarget, results: [MediaEditorScreenResult], existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void) + func proceedWithStoryUpload(target: Stories.PendingTarget, results: [MediaEditorScreenResult], existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, folders: [Int64], externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void) func getContactsController() -> ViewController? func getChatsController() -> ViewController? diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 0bf257604c..d42bc0ead8 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -992,8 +992,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[872932635] = { return Api.StickerSetCovered.parse_stickerSetMultiCovered($0) } dict[2008112412] = { return Api.StickerSetCovered.parse_stickerSetNoCovered($0) } dict[1898850301] = { return Api.StoriesStealthMode.parse_storiesStealthMode($0) } + dict[-1826262950] = { return Api.StoryAlbum.parse_storyAlbum($0) } dict[-1205411504] = { return Api.StoryFwdHeader.parse_storyFwdHeader($0) } - dict[2041735716] = { return Api.StoryItem.parse_storyItem($0) } + dict[-302947087] = { return Api.StoryItem.parse_storyItem($0) } dict[1374088783] = { return Api.StoryItem.parse_storyItemDeleted($0) } dict[-5388013] = { return Api.StoryItem.parse_storyItemSkipped($0) } dict[1620104917] = { return Api.StoryReaction.parse_storyReaction($0) } @@ -1476,6 +1477,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[172975040] = { return Api.storage.FileType.parse_filePng($0) } dict[-1432995067] = { return Api.storage.FileType.parse_fileUnknown($0) } dict[276907596] = { return Api.storage.FileType.parse_fileWebp($0) } + dict[-1013417414] = { return Api.stories.Albums.parse_albums($0) } + dict[1448008427] = { return Api.stories.Albums.parse_albumsNotModified($0) } dict[1862033025] = { return Api.stories.AllStories.parse_allStories($0) } dict[291044926] = { return Api.stories.AllStories.parse_allStoriesNotModified($0) } dict[-1014513586] = { return Api.stories.CanSendStoryCount.parse_canSendStoryCount($0) } @@ -2196,6 +2199,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.StoriesStealthMode: _1.serialize(buffer, boxed) + case let _1 as Api.StoryAlbum: + _1.serialize(buffer, boxed) case let _1 as Api.StoryFwdHeader: _1.serialize(buffer, boxed) case let _1 as Api.StoryItem: @@ -2614,6 +2619,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.storage.FileType: _1.serialize(buffer, boxed) + case let _1 as Api.stories.Albums: + _1.serialize(buffer, boxed) case let _1 as Api.stories.AllStories: _1.serialize(buffer, boxed) case let _1 as Api.stories.CanSendStoryCount: diff --git a/submodules/TelegramApi/Sources/Api26.swift b/submodules/TelegramApi/Sources/Api26.swift index dff9d15574..c2d30c953f 100644 --- a/submodules/TelegramApi/Sources/Api26.swift +++ b/submodules/TelegramApi/Sources/Api26.swift @@ -324,6 +324,62 @@ public extension Api { } } +public extension Api { + enum StoryAlbum: TypeConstructorDescription { + case storyAlbum(flags: Int32, albumId: Int32, title: String, iconPhoto: Api.Photo?, iconVideo: Api.Document?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .storyAlbum(let flags, let albumId, let title, let iconPhoto, let iconVideo): + if boxed { + buffer.appendInt32(-1826262950) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(albumId, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {iconPhoto!.serialize(buffer, true)} + if Int(flags) & Int(1 << 1) != 0 {iconVideo!.serialize(buffer, true)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .storyAlbum(let flags, let albumId, let title, let iconPhoto, let iconVideo): + return ("storyAlbum", [("flags", flags as Any), ("albumId", albumId as Any), ("title", title as Any), ("iconPhoto", iconPhoto as Any), ("iconVideo", iconVideo as Any)]) + } + } + + public static func parse_storyAlbum(_ reader: BufferReader) -> StoryAlbum? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + var _4: Api.Photo? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.Photo + } } + var _5: Api.Document? + if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _5 = Api.parse(reader, signature: signature) as? Api.Document + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.StoryAlbum.storyAlbum(flags: _1!, albumId: _2!, title: _3!, iconPhoto: _4, iconVideo: _5) + } + else { + return nil + } + } + + } +} public extension Api { enum StoryFwdHeader: TypeConstructorDescription { case storyFwdHeader(flags: Int32, from: Api.Peer?, fromName: String?, storyId: Int32?) @@ -376,15 +432,15 @@ public extension Api { } public extension Api { indirect enum StoryItem: TypeConstructorDescription { - case storyItem(flags: Int32, id: Int32, date: Int32, fromId: Api.Peer?, fwdFrom: Api.StoryFwdHeader?, expireDate: Int32, caption: String?, entities: [Api.MessageEntity]?, media: Api.MessageMedia, mediaAreas: [Api.MediaArea]?, privacy: [Api.PrivacyRule]?, views: Api.StoryViews?, sentReaction: Api.Reaction?) + case storyItem(flags: Int32, id: Int32, date: Int32, fromId: Api.Peer?, fwdFrom: Api.StoryFwdHeader?, expireDate: Int32, caption: String?, entities: [Api.MessageEntity]?, media: Api.MessageMedia, mediaAreas: [Api.MediaArea]?, privacy: [Api.PrivacyRule]?, views: Api.StoryViews?, sentReaction: Api.Reaction?, albums: [Int32]?) case storyItemDeleted(id: Int32) case storyItemSkipped(flags: Int32, id: Int32, date: Int32, expireDate: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .storyItem(let flags, let id, let date, let fromId, let fwdFrom, let expireDate, let caption, let entities, let media, let mediaAreas, let privacy, let views, let sentReaction): + case .storyItem(let flags, let id, let date, let fromId, let fwdFrom, let expireDate, let caption, let entities, let media, let mediaAreas, let privacy, let views, let sentReaction, let albums): if boxed { - buffer.appendInt32(2041735716) + buffer.appendInt32(-302947087) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(id, buffer: buffer, boxed: false) @@ -411,6 +467,11 @@ public extension Api { }} if Int(flags) & Int(1 << 3) != 0 {views!.serialize(buffer, true)} if Int(flags) & Int(1 << 15) != 0 {sentReaction!.serialize(buffer, true)} + if Int(flags) & Int(1 << 19) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(albums!.count)) + for item in albums! { + serializeInt32(item, buffer: buffer, boxed: false) + }} break case .storyItemDeleted(let id): if boxed { @@ -432,8 +493,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .storyItem(let flags, let id, let date, let fromId, let fwdFrom, let expireDate, let caption, let entities, let media, let mediaAreas, let privacy, let views, let sentReaction): - return ("storyItem", [("flags", flags as Any), ("id", id as Any), ("date", date as Any), ("fromId", fromId as Any), ("fwdFrom", fwdFrom as Any), ("expireDate", expireDate as Any), ("caption", caption as Any), ("entities", entities as Any), ("media", media as Any), ("mediaAreas", mediaAreas as Any), ("privacy", privacy as Any), ("views", views as Any), ("sentReaction", sentReaction as Any)]) + case .storyItem(let flags, let id, let date, let fromId, let fwdFrom, let expireDate, let caption, let entities, let media, let mediaAreas, let privacy, let views, let sentReaction, let albums): + return ("storyItem", [("flags", flags as Any), ("id", id as Any), ("date", date as Any), ("fromId", fromId as Any), ("fwdFrom", fwdFrom as Any), ("expireDate", expireDate as Any), ("caption", caption as Any), ("entities", entities as Any), ("media", media as Any), ("mediaAreas", mediaAreas as Any), ("privacy", privacy as Any), ("views", views as Any), ("sentReaction", sentReaction as Any), ("albums", albums as Any)]) case .storyItemDeleted(let id): return ("storyItemDeleted", [("id", id as Any)]) case .storyItemSkipped(let flags, let id, let date, let expireDate): @@ -484,6 +545,10 @@ public extension Api { if Int(_1!) & Int(1 << 15) != 0 {if let signature = reader.readInt32() { _13 = Api.parse(reader, signature: signature) as? Api.Reaction } } + var _14: [Int32]? + if Int(_1!) & Int(1 << 19) != 0 {if let _ = reader.readInt32() { + _14 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -497,8 +562,9 @@ public extension Api { let _c11 = (Int(_1!) & Int(1 << 2) == 0) || _11 != nil let _c12 = (Int(_1!) & Int(1 << 3) == 0) || _12 != nil let _c13 = (Int(_1!) & Int(1 << 15) == 0) || _13 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 { - return Api.StoryItem.storyItem(flags: _1!, id: _2!, date: _3!, fromId: _4, fwdFrom: _5, expireDate: _6!, caption: _7, entities: _8, media: _9!, mediaAreas: _10, privacy: _11, views: _12, sentReaction: _13) + let _c14 = (Int(_1!) & Int(1 << 19) == 0) || _14 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 { + return Api.StoryItem.storyItem(flags: _1!, id: _2!, date: _3!, fromId: _4, fwdFrom: _5, expireDate: _6!, caption: _7, entities: _8, media: _9!, mediaAreas: _10, privacy: _11, views: _12, sentReaction: _13, albums: _14) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api37.swift b/submodules/TelegramApi/Sources/Api37.swift index 62bcb3429a..cbba07a113 100644 --- a/submodules/TelegramApi/Sources/Api37.swift +++ b/submodules/TelegramApi/Sources/Api37.swift @@ -482,6 +482,64 @@ public extension Api.storage { } } +public extension Api.stories { + enum Albums: TypeConstructorDescription { + case albums(hash: Int64, albums: [Api.StoryAlbum]) + case albumsNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .albums(let hash, let albums): + if boxed { + buffer.appendInt32(-1013417414) + } + serializeInt64(hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(albums.count)) + for item in albums { + item.serialize(buffer, true) + } + break + case .albumsNotModified: + if boxed { + buffer.appendInt32(1448008427) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .albums(let hash, let albums): + return ("albums", [("hash", hash as Any), ("albums", albums as Any)]) + case .albumsNotModified: + return ("albumsNotModified", []) + } + } + + public static func parse_albums(_ reader: BufferReader) -> Albums? { + var _1: Int64? + _1 = reader.readInt64() + var _2: [Api.StoryAlbum]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryAlbum.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.stories.Albums.albums(hash: _1!, albums: _2!) + } + else { + return nil + } + } + public static func parse_albumsNotModified(_ reader: BufferReader) -> Albums? { + return Api.stories.Albums.albumsNotModified + } + + } +} public extension Api.stories { enum AllStories: TypeConstructorDescription { case allStories(flags: Int32, count: Int32, state: String, peerStories: [Api.PeerStories], chats: [Api.Chat], users: [Api.User], stealthMode: Api.StoriesStealthMode) @@ -1428,55 +1486,3 @@ public extension Api.updates { } } -public extension Api.updates { - enum State: TypeConstructorDescription { - case state(pts: Int32, qts: Int32, date: Int32, seq: Int32, unreadCount: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .state(let pts, let qts, let date, let seq, let unreadCount): - if boxed { - buffer.appendInt32(-1519637954) - } - serializeInt32(pts, buffer: buffer, boxed: false) - serializeInt32(qts, buffer: buffer, boxed: false) - serializeInt32(date, buffer: buffer, boxed: false) - serializeInt32(seq, buffer: buffer, boxed: false) - serializeInt32(unreadCount, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .state(let pts, let qts, let date, let seq, let unreadCount): - return ("state", [("pts", pts as Any), ("qts", qts as Any), ("date", date as Any), ("seq", seq as Any), ("unreadCount", unreadCount as Any)]) - } - } - - public static func parse_state(_ reader: BufferReader) -> State? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - _4 = reader.readInt32() - var _5: Int32? - _5 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.updates.State.state(pts: _1!, qts: _2!, date: _3!, seq: _4!, unreadCount: _5!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api38.swift b/submodules/TelegramApi/Sources/Api38.swift index e76c07649a..281a495dbf 100644 --- a/submodules/TelegramApi/Sources/Api38.swift +++ b/submodules/TelegramApi/Sources/Api38.swift @@ -1,3 +1,55 @@ +public extension Api.updates { + enum State: TypeConstructorDescription { + case state(pts: Int32, qts: Int32, date: Int32, seq: Int32, unreadCount: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .state(let pts, let qts, let date, let seq, let unreadCount): + if boxed { + buffer.appendInt32(-1519637954) + } + serializeInt32(pts, buffer: buffer, boxed: false) + serializeInt32(qts, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + serializeInt32(seq, buffer: buffer, boxed: false) + serializeInt32(unreadCount, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .state(let pts, let qts, let date, let seq, let unreadCount): + return ("state", [("pts", pts as Any), ("qts", qts as Any), ("date", date as Any), ("seq", seq as Any), ("unreadCount", unreadCount as Any)]) + } + } + + public static func parse_state(_ reader: BufferReader) -> State? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.updates.State.state(pts: _1!, qts: _2!, date: _3!, seq: _4!, unreadCount: _5!) + } + else { + return nil + } + } + + } +} public extension Api.upload { enum CdnFile: TypeConstructorDescription { case cdnFile(bytes: Buffer) diff --git a/submodules/TelegramApi/Sources/Api39.swift b/submodules/TelegramApi/Sources/Api39.swift index 43e04f36f2..5567cfe4d7 100644 --- a/submodules/TelegramApi/Sources/Api39.swift +++ b/submodules/TelegramApi/Sources/Api39.swift @@ -11371,6 +11371,43 @@ public extension Api.functions.stories { }) } } +public extension Api.functions.stories { + static func createAlbum(peer: Api.InputPeer, title: String, stories: [Int32]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1553754395) + peer.serialize(buffer, true) + serializeString(title, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(stories.count)) + for item in stories { + serializeInt32(item, buffer: buffer, boxed: false) + } + return (FunctionDescription(name: "stories.createAlbum", parameters: [("peer", String(describing: peer)), ("title", String(describing: title)), ("stories", String(describing: stories))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.StoryAlbum? in + let reader = BufferReader(buffer) + var result: Api.StoryAlbum? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.StoryAlbum + } + return result + }) + } +} +public extension Api.functions.stories { + static func deleteAlbum(peer: Api.InputPeer, albumId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1925949744) + peer.serialize(buffer, true) + serializeInt32(albumId, buffer: buffer, boxed: false) + return (FunctionDescription(name: "stories.deleteAlbum", parameters: [("peer", String(describing: peer)), ("albumId", String(describing: albumId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.stories { static func deleteStories(peer: Api.InputPeer, id: [Int32]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Int32]>) { let buffer = Buffer() @@ -11441,6 +11478,40 @@ public extension Api.functions.stories { }) } } +public extension Api.functions.stories { + static func getAlbumStories(peer: Api.InputPeer, albumId: Int32, offset: Int32, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1400869535) + peer.serialize(buffer, true) + serializeInt32(albumId, buffer: buffer, boxed: false) + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(limit, buffer: buffer, boxed: false) + return (FunctionDescription(name: "stories.getAlbumStories", parameters: [("peer", String(describing: peer)), ("albumId", String(describing: albumId)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.Stories? in + let reader = BufferReader(buffer) + var result: Api.stories.Stories? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.stories.Stories + } + return result + }) + } +} +public extension Api.functions.stories { + static func getAlbums(peer: Api.InputPeer, hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(632548039) + peer.serialize(buffer, true) + serializeInt64(hash, buffer: buffer, boxed: false) + return (FunctionDescription(name: "stories.getAlbums", parameters: [("peer", String(describing: peer)), ("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.Albums? in + let reader = BufferReader(buffer) + var result: Api.stories.Albums? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.stories.Albums + } + return result + }) + } +} public extension Api.functions.stories { static func getAllReadPeerStories() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -11671,6 +11742,26 @@ public extension Api.functions.stories { }) } } +public extension Api.functions.stories { + static func reorderAlbums(peer: Api.InputPeer, order: [Int32]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-2060059687) + peer.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(order.count)) + for item in order { + serializeInt32(item, buffer: buffer, boxed: false) + } + return (FunctionDescription(name: "stories.reorderAlbums", parameters: [("peer", String(describing: peer)), ("order", String(describing: order))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.stories { static func report(peer: Api.InputPeer, id: [Int32], option: Buffer, message: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -11732,9 +11823,9 @@ public extension Api.functions.stories { } } public extension Api.functions.stories { - static func sendStory(flags: Int32, peer: Api.InputPeer, media: Api.InputMedia, mediaAreas: [Api.MediaArea]?, caption: String?, entities: [Api.MessageEntity]?, privacyRules: [Api.InputPrivacyRule], randomId: Int64, period: Int32?, fwdFromId: Api.InputPeer?, fwdFromStory: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func sendStory(flags: Int32, peer: Api.InputPeer, media: Api.InputMedia, mediaAreas: [Api.MediaArea]?, caption: String?, entities: [Api.MessageEntity]?, privacyRules: [Api.InputPrivacyRule], randomId: Int64, period: Int32?, fwdFromId: Api.InputPeer?, fwdFromStory: Int32?, albums: [Int32]?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-454661813) + buffer.appendInt32(1937752812) serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) media.serialize(buffer, true) @@ -11758,7 +11849,12 @@ public extension Api.functions.stories { if Int(flags) & Int(1 << 3) != 0 {serializeInt32(period!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 6) != 0 {fwdFromId!.serialize(buffer, true)} if Int(flags) & Int(1 << 6) != 0 {serializeInt32(fwdFromStory!, buffer: buffer, boxed: false)} - return (FunctionDescription(name: "stories.sendStory", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("media", String(describing: media)), ("mediaAreas", String(describing: mediaAreas)), ("caption", String(describing: caption)), ("entities", String(describing: entities)), ("privacyRules", String(describing: privacyRules)), ("randomId", String(describing: randomId)), ("period", String(describing: period)), ("fwdFromId", String(describing: fwdFromId)), ("fwdFromStory", String(describing: fwdFromStory))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + if Int(flags) & Int(1 << 8) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(albums!.count)) + for item in albums! { + serializeInt32(item, buffer: buffer, boxed: false) + }} + return (FunctionDescription(name: "stories.sendStory", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("media", String(describing: media)), ("mediaAreas", String(describing: mediaAreas)), ("caption", String(describing: caption)), ("entities", String(describing: entities)), ("privacyRules", String(describing: privacyRules)), ("randomId", String(describing: randomId)), ("period", String(describing: period)), ("fwdFromId", String(describing: fwdFromId)), ("fwdFromStory", String(describing: fwdFromStory)), ("albums", String(describing: albums))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { @@ -11840,6 +11936,39 @@ public extension Api.functions.stories { }) } } +public extension Api.functions.stories { + static func updateAlbum(flags: Int32, peer: Api.InputPeer, albumId: Int32, title: String?, deleteStories: [Int32]?, addStories: [Int32]?, order: [Int32]?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1582455222) + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + serializeInt32(albumId, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(title!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(deleteStories!.count)) + for item in deleteStories! { + serializeInt32(item, buffer: buffer, boxed: false) + }} + if Int(flags) & Int(1 << 2) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(addStories!.count)) + for item in addStories! { + serializeInt32(item, buffer: buffer, boxed: false) + }} + if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(order!.count)) + for item in order! { + serializeInt32(item, buffer: buffer, boxed: false) + }} + return (FunctionDescription(name: "stories.updateAlbum", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("albumId", String(describing: albumId)), ("title", String(describing: title)), ("deleteStories", String(describing: deleteStories)), ("addStories", String(describing: addStories)), ("order", String(describing: order))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.StoryAlbum? in + let reader = BufferReader(buffer) + var result: Api.StoryAlbum? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.StoryAlbum + } + return result + }) + } +} public extension Api.functions.updates { static func getChannelDifference(flags: Int32, channel: Api.InputChannel, filter: Api.ChannelMessagesFilter, pts: Int32, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index ddd61c801e..1cda543eba 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 210 + return 211 } public func parseMessage(_ data: Data!) -> Any! { 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 59e1183adb..55d981e40b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -988,7 +988,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 +1017,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 +1065,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 +1143,7 @@ func _internal_uploadStoryImpl( privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, period: Int, + folders: [Int64], randomId: Int64, forwardInfo: Stories.PendingForwardInfo? ) -> Signal { @@ -1224,6 +1227,10 @@ func _internal_uploadStoryImpl( fwdFromStory = forwardInfo.storyId } + if !folders.isEmpty { + flags |= 1 << 8 + } + return network.request(Api.functions.stories.sendStory( flags: flags, peer: inputPeer, @@ -1235,7 +1242,8 @@ func _internal_uploadStoryImpl( randomId: randomId, period: Int32(period), fwdFromId: fwdFromId, - fwdFromStory: fwdFromStory + fwdFromStory: fwdFromStory, + albums: folders.isEmpty ? nil : folders.map(Int32.init(clamping:)) )) |> map(Optional.init) |> `catch` { _ -> Signal in @@ -1259,7 +1267,7 @@ func _internal_uploadStoryImpl( for update in updates.allUpdates { if case let .updateStory(_, story) = update { switch story { - case let .storyItem(_, idValue, _, fromId, _, _, _, _, media, _, _, _, _): + case let .storyItem(_, idValue, _, fromId, _, _, _, _, media, _, _, _, _, _): if let parsedStory = Stories.StoredItem(apiStoryItem: story, peerId: toPeerId, transaction: transaction) { var items = transaction.getStoryItems(peerId: toPeerId) var updatedItems: [Stories.Item] = [] @@ -1648,7 +1656,7 @@ func _internal_editStory(account: Account, peerId: PeerId, id: Int32, media: Eng for update in updates.allUpdates { if case let .updateStory(_, story) = update { switch story { - case let .storyItem(_, _, _, _, _, _, _, _, media, _, _, _, _): + case let .storyItem(_, _, _, _, _, _, _, _, media, _, _, _, _, _): let parsedMedia = textMediaAndExpirationTimerFromApiMedia(media, account.peerId).media if let parsedMedia = parsedMedia, let originalMedia = originalMedia { applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: account.postbox, force: false, skipPreviews: updatingCoverTime) @@ -2009,7 +2017,7 @@ func _internal_updatePinnedToTopStories(account: Account, peerId: PeerId, ids: [ extension Api.StoryItem { var id: Int32 { switch self { - case let .storyItem(_, id, _, _, _, _, _, _, _, _, _, _, _): + case let .storyItem(_, id, _, _, _, _, _, _, _, _, _, _, _, _): return id case let .storyItemDeleted(id): return id @@ -2072,7 +2080,9 @@ 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): + let _ = albums + let parsedMedia = textMediaAndExpirationTimerFromApiMedia(media, peerId).media if let parsedMedia = parsedMedia { var parsedPrivacy: Stories.Item.Privacy? diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index c0c957fe6d..320bebaedd 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -679,6 +679,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) @@ -768,7 +770,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 +830,73 @@ public final class PeerStoryListContext: StoryListContext { let accountPeerId = account.peerId let isArchived = self.isArchived let folderId = self.folderId - let folders = self.stateValue.availableFolders 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))) + let 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 - } - |> `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)) + 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 { @@ -985,20 +957,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(self.queue)).start(next: { [weak self] storyItems, totalCount, peerReference, folderItems, hasMore in guard let self else { return } @@ -1022,6 +1012,8 @@ public final class PeerStoryListContext: StoryListContext { updatedState.items.append(item) } + updatedState.availableFolders = folderItems + if updatedState.peerReference == nil { updatedState.peerReference = peerReference } @@ -1376,55 +1368,93 @@ 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, 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: [])) + |> 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) + } + } |> 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 reorderFolders(ids: [Int64]) -> Disposable { var state = self.stateValue var folders: [State.Folder] = [] for id in ids { @@ -1440,24 +1470,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) @@ -1528,26 +1585,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 +1639,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 +1717,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 +1766,92 @@ public final class PeerStoryListContext: StoryListContext { } } - public func addFolder(title: String, completion: @escaping (Int64) -> Void) { + public func addFolder(title: String, completion: @escaping (Int64?) -> Void) -> Disposable { + let disposable = MetaDisposable() self.impl.with { impl in - completion(impl.addFolder(title: title)) + disposable.set(impl.addFolder(title: title, 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 reorderFolders(ids: [Int64]) -> Disposable { + let disposable = MetaDisposable() self.impl.with { impl in - impl.reorderFolders(ids: ids) + disposable.set(impl.reorderFolders(ids: ids)) } + return disposable } - public func addToFolder(id: Int64, items: [EngineStoryItem]) { + public func addToFolder(id: Int64, items: [EngineStoryItem]) -> Disposable { + let disposable = MetaDisposable() self.impl.with { impl in - impl.addToFolder(id: id, items: items) + disposable.set(impl.addToFolder(id: id, items: items)) } + return disposable } - public func removeFromFolder(id: Int64, itemIds: [Int32]) { + public func removeFromFolder(id: Int64, itemIds: [Int32]) -> Disposable { + let disposable = MetaDisposable() self.impl.with { impl in - impl.removeFromFolder(id: id, itemIds: itemIds) + disposable.set(impl.removeFromFolder(id: id, itemIds: itemIds)) } + return disposable } - public func reorderItemsInFolder(itemIds: [Int32]) { + public func reorderItemsInFolder(itemIds: [Int32]) -> Disposable { + let disposable = MetaDisposable() self.impl.with { impl in - impl.reorderItemsInFolder(itemIds: itemIds) + disposable.set(impl.reorderItemsInFolder(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 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> { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index ae90822692..0ed3892780 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -1367,8 +1367,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/MediaEditorScreen/Sources/EditStories.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift index e8cc73e516..3359bf0c1d 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift @@ -163,7 +163,7 @@ public extension MediaEditorScreenImpl { } else { existingMedia = storyItem.media } - rootController.proceedWithStoryUpload(target: target, results: [result as! MediaEditorScreenResult], existingMedia: existingMedia, forwardInfo: forwardInfo, externalState: externalState, commit: commit) + rootController.proceedWithStoryUpload(target: target, results: [result as! MediaEditorScreenResult], existingMedia: existingMedia, forwardInfo: forwardInfo, folders: [], externalState: externalState, commit: commit) } }) } else { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 400e9140a8..06d2feb66b 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -10258,7 +10258,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro viewControllers = viewControllers.filter { !($0 is AttachmentController)} rootController.setViewControllers(viewControllers, animated: false) - rootController.proceedWithStoryUpload(target: target, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) + rootController.proceedWithStoryUpload(target: target, results: [result], existingMedia: nil, forwardInfo: nil, folders: [], externalState: externalState, commit: commit) } }, cancelled: {} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 6814d08189..7f1cdd3f54 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -2481,7 +2481,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } if let listSource = self.listSource as? PeerStoryListContext { - listSource.removeFromFolder(id: folder.id, itemIds: [item.id]) + let _ = listSource.removeFromFolder(id: folder.id, itemIds: [item.id]) } f(.dismissWithoutContent) @@ -2575,7 +2575,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr c?.dismiss(completion: {}) if let listSource = self.listSource as? PeerStoryListContext { - listSource.addToFolder(id: folderPreview.folder.id, items: [item]) + let _ = listSource.addToFolder(id: folderPreview.folder.id, items: [item]) } }))) } @@ -4900,13 +4900,13 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } if let value { if let listSource = self.listSource as? PeerStoryListContext { - listSource.addFolder(title: value, completion: { [weak self] id in + let _ = listSource.addFolder(title: value, completion: { [weak self] id in Queue.mainQueue().async { - guard let self, let listSource = self.listSource as? PeerStoryListContext else { + guard let self, let id, let listSource = self.listSource as? PeerStoryListContext else { return } if !addItems.isEmpty { - listSource.addToFolder(id: id, items: addItems) + let _ = listSource.addToFolder(id: id, items: addItems) } self.setStoryFolder(id: id, assumeEmpty: addItems.isEmpty) } @@ -4931,7 +4931,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr return } if let listSource = self.listSource as? PeerStoryListContext { - listSource.addToFolder(id: folder.id, items: items) + let _ = listSource.addToFolder(id: folder.id, items: items) } }) controller.navigationPresentation = .modal @@ -4958,7 +4958,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.removedStoryFolders.insert(id) if let listContext = self.listSource as? PeerStoryListContext { - listContext.removeFolder(id: id) + let _ = listContext.removeFolder(id: id) } if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams { @@ -5111,7 +5111,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if !isReordering && !self.currentStoryFolders.isEmpty && self.canManageStories { if let listSource = self.listSource as? PeerStoryListContext { - listSource.reorderFolders(ids: self.currentStoryFolders.map(\.id)) + let _ = listSource.reorderFolders(ids: self.currentStoryFolders.map(\.id)) } } @@ -5132,7 +5132,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } else if case let .peer(id, _, _) = self.scope, id == self.context.account.peerId, let items = self.items { if let _ = self.currentStoryFolder { if let listSource = self.listSource as? PeerStoryListContext { - listSource.reorderItemsInFolder(itemIds: reorderedIds.map { $0.id }) + let _ = listSource.reorderItemsInFolder(itemIds: reorderedIds.map { $0.id }) } } else { var updatedPinnedIds: [Int32] = [] diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index cf04937b71..5cb946e270 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -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) + }) } } ) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index b22b6aefb3..df2e7e0a69 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, 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/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index f24d0624f1..6d1ece1b61 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -992,7 +992,7 @@ func openResolvedUrlImpl( if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { rootController.popToRoot(animated: false) - rootController.proceedWithStoryUpload(target: target, results: results, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) + rootController.proceedWithStoryUpload(target: target, results: results, existingMedia: nil, forwardInfo: nil, folders: [], externalState: externalState, commit: commit) } }) if let navigationController { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 48ad0ae251..b8eb922eb4 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -3821,7 +3821,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { externalState.storyTarget = target if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { - rootController.proceedWithStoryUpload(target: target, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) + rootController.proceedWithStoryUpload(target: target, results: [result], existingMedia: nil, forwardInfo: nil, folders: [], externalState: externalState, commit: commit) } let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: targetPeerId)) diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 683e3bb911..0f743f7c67 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -471,7 +471,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon if let customTarget, case .botPreview = customTarget { externalState.storyTarget = customTarget - self.proceedWithStoryUpload(target: customTarget, results: results, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) + self.proceedWithStoryUpload(target: customTarget, results: results, existingMedia: nil, forwardInfo: nil, folders: [], externalState: externalState, commit: commit) dismissCameraImpl?() return @@ -504,7 +504,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, folders: [], externalState: externalState, commit: commit) dismissCameraImpl?() }) @@ -568,7 +568,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon }) } - public func proceedWithStoryUpload(target: Stories.PendingTarget, results: [MediaEditorScreenResult], existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void) { + public func proceedWithStoryUpload(target: Stories.PendingTarget, results: [MediaEditorScreenResult], existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, folders: [Int64], externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void) { guard let results = results as? [MediaEditorScreenImpl.Result] else { return } @@ -753,6 +753,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/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 3445a10613..aaa520fd2d 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -1494,7 +1494,7 @@ public final class WebAppController: ViewController, AttachmentContainable { externalState.storyTarget = target if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { - rootController.proceedWithStoryUpload(target: target, results: results, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) + rootController.proceedWithStoryUpload(target: target, results: results, existingMedia: nil, forwardInfo: nil, folders: [], externalState: externalState, commit: commit) } }) if let navigationController = self.controller?.getNavigationController() { From a5ad030123688c4ce2b56a8a2737f6186c7ab455 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 22 Jul 2025 16:08:51 +0200 Subject: [PATCH 05/11] Update --- .../Sources/AccountContext.swift | 2 + .../Sources/HashtagSearchControllerNode.swift | 3 +- submodules/Postbox/Sources/Postbox.swift | 2 +- .../State/AccountStateManagementUtils.swift | 6 +- .../Sources/Statistics/StoryStatistics.swift | 3 +- .../Messages/EngineStoryViewListContext.swift | 12 +- .../TelegramEngine/Messages/Stories.swift | 43 +++- .../Messages/StoryListContext.swift | 75 ++++-- .../Messages/TelegramEngineMessages.swift | 3 +- .../ChatRecentActionsControllerNode.swift | 2 + .../Sources/PeerInfoPaneContainerNode.swift | 21 +- .../Sources/PeerInfoScreen.swift | 35 ++- .../Sources/PeerInfoStoryGridScreen.swift | 3 +- .../Sources/StorySearchGridScreen.swift | 3 +- .../Sources/PeerInfoStoryPaneNode.swift | 237 ++++++++++++------ .../Sources/StoryChatContent.swift | 12 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 21 ++ .../Sources/SharedAccountContext.swift | 5 +- .../UrlHandling/Sources/UrlHandling.swift | 7 + 19 files changed, 357 insertions(+), 138 deletions(-) diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index fde006cf19..2df656d83a 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/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/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 55d981e40b..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 } } @@ -1294,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)) @@ -1702,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) @@ -1734,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) @@ -1930,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) @@ -1961,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) } @@ -2081,7 +2095,10 @@ 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, albums): - let _ = albums + var folderIds: [Int64]? + if let albums { + folderIds = albums.map(Int64.init) + } let parsedMedia = textMediaAndExpirationTimerFromApiMedia(media, peerId).media if let parsedMedia = parsedMedia { @@ -2194,7 +2211,8 @@ extension Stories.StoredItem { isMy: mergedIsMy, myReaction: mergedMyReaction, forwardInfo: mergedForwardInfo, - authorId: fromId?.peerId + authorId: fromId?.peerId, + folderIds: folderIds ) self = .item(item) } else { @@ -2271,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 ) } } @@ -2748,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) { @@ -2782,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 320bebaedd..6479c83182 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 ) } } @@ -735,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), @@ -830,6 +837,7 @@ public final class PeerStoryListContext: StoryListContext { let accountPeerId = account.peerId let isArchived = self.isArchived let folderId = self.folderId + let queue = self.queue self.requestDisposable = (self.account.postbox.transaction { transaction -> Api.InputPeer? in return transaction.getPeer(peerId).flatMap(apiInputPeer) } @@ -838,7 +846,7 @@ public final class PeerStoryListContext: StoryListContext { return .single(([], 0, nil, [], false)) } - let signal: Signal + 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) @@ -851,6 +859,13 @@ public final class PeerStoryListContext: StoryListContext { |> map(Optional.init) } } + + #if DEBUG + if folderId != nil { + signal = signal |> delay(2.0, queue: queue) + } + #endif + return combineLatest( signal |> `catch` { _ -> Signal in @@ -945,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), @@ -988,7 +1004,7 @@ public final class PeerStoryListContext: StoryListContext { } } } - |> deliverOn(self.queue)).start(next: { [weak self] storyItems, totalCount, peerReference, folderItems, hasMore in + |> deliverOn(queue)).start(next: { [weak self] storyItems, totalCount, peerReference, folderItems, hasMore in guard let self else { return } @@ -1139,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 } @@ -1188,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 { @@ -1239,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) @@ -1296,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) @@ -1575,7 +1595,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) @@ -1910,7 +1931,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 ) } } @@ -2092,7 +2114,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), @@ -2109,7 +2132,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 } @@ -2241,7 +2264,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 ) @@ -2302,7 +2326,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 ) @@ -2431,7 +2456,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)) } @@ -2877,7 +2903,8 @@ public final class BotPreviewStoryListContext: StoryListContext { isMy: false, myReaction: nil, forwardInfo: nil, - author: nil + author: nil, + folderIds: nil ), peer: nil )) @@ -2926,7 +2953,8 @@ public final class BotPreviewStoryListContext: StoryListContext { isMy: false, myReaction: nil, forwardInfo: nil, - author: nil + author: nil, + folderIds: nil ), peer: nil )) @@ -3038,7 +3066,8 @@ public final class BotPreviewStoryListContext: StoryListContext { isMy: false, myReaction: nil, forwardInfo: nil, - author: nil + author: nil, + folderIds: nil ), peer: nil )) @@ -3115,7 +3144,8 @@ public final class BotPreviewStoryListContext: StoryListContext { isMy: false, myReaction: nil, forwardInfo: nil, - author: nil + author: nil, + folderIds: nil ), peer: nil )) @@ -3178,7 +3208,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 0ed3892780..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) 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/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 06d2feb66b..1c56d9d1af 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -2906,6 +2906,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 @@ -3030,7 +3031,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 @@ -3046,6 +3047,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() @@ -3057,7 +3059,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() @@ -5392,7 +5394,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) @@ -5403,7 +5405,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)) @@ -11710,6 +11712,22 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } }))) + 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) @@ -13082,6 +13100,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) @@ -13157,7 +13176,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 @@ -13175,6 +13195,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 { @@ -13540,8 +13561,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 7f1cdd3f54..41196da9f2 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -1652,7 +1652,7 @@ 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.isProfileEmbedded { + } 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 @@ -1686,6 +1686,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr private let defaultListSource: 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 { return nil @@ -1741,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): @@ -2471,7 +2474,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr var items: [ContextMenuItem] = [] if canManage, case let .peer(peerId, _, isArchived) = self.scope { - if peerId == self.context.account.peerId && self.isProfileEmbedded { + 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 @@ -2851,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 } @@ -2945,34 +2948,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 || 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) + + 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 } }) } @@ -3021,8 +3031,8 @@ 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 { + case .peer: + if self.canManageStories { if self.currentStoryFolder != nil { isReorderable = true } else { @@ -3054,7 +3064,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 } @@ -3130,7 +3140,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } 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 { + } 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) } @@ -3606,7 +3616,7 @@ 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 let .peer(peerId, _, isArchived) = self.scope, peerId == self.context.account.peerId, !isArchived, self.isProfileEmbedded { + 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) @@ -3769,7 +3779,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr 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.isProfileEmbedded { + } else if case let .peer(peerId, _, isArchived) = self.scope, ((peerId == self.context.account.peerId && !isArchived) || !self.currentStoryFolders.isEmpty), self.isProfileEmbedded { displayFolderTab = true } @@ -3777,10 +3787,13 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr return } + var transition = transition + let folderTab: ComponentView if let current = self.folderTab { folderTab = current } else { + transition = .immediate folderTab = ComponentView() self.folderTab = folderTab } @@ -3814,7 +3827,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr id: AnyHashable(folder.id), title: folder.title, isReorderable: self.canManageStories, - contextAction: self.canManageStories ? { [weak self] sourceNode, gesture in + contextAction: { [weak self] sourceNode, gesture in guard let self else { return } @@ -3827,36 +3840,24 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr var items: [ContextMenuItem] = [] - //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 { + 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) - return - } - - a(.default) - - self.presentAddStoriesToFolder() - }))) + + self.presentAddStoriesToFolder() + }))) + } //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 + 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 @@ -3864,9 +3865,37 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr f(.dismissWithoutContent) - self.presentDeleteStoryFolder(id: folder.id) + 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, @@ -3880,14 +3909,16 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr gesture: gesture ) controller.presentInGlobalOverlay(contextController) - } : nil + } )) } } - folderItems.append(TabSelectorComponent.Item( - id: AnyHashable("_add"), - title: addTitle - )) + if self.canManageStories { + folderItems.append(TabSelectorComponent.Item( + id: AnyHashable("_add"), + title: addTitle + )) + } var selectedId = AnyHashable("_main") if let listSource = self.listSource as? BotPreviewStoryListContext, let language = listSource.language { selectedId = AnyHashable(language) @@ -4113,7 +4144,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr 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 { + } else if case let .peer(id, _, isArchived) = self.scope, ((id == self.context.account.peerId && !isArchived) || !self.currentStoryFolders.isEmpty) { hasBarBackground = true } } @@ -4136,7 +4167,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr 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.isProfileEmbedded { + } 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 } @@ -4485,14 +4516,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr 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, + actionTitle: (isArchived || !self.canManageStories) ? nil : presentationData.strings.StoryList_SavedAddAction, action: { [weak self] in guard let self else { return } self.emptyAction?() }, - additionalActionTitle: (isArchived || self.isProfileEmbedded) ? nil : presentationData.strings.StoryList_SavedEmptyAction, + additionalActionTitle: (isArchived || self.isProfileEmbedded || !self.canManageStories) ? nil : presentationData.strings.StoryList_SavedEmptyAction, additionalAction: { [weak self] in guard let self else { return @@ -4716,7 +4747,7 @@ 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.isProfileEmbedded { + } 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) @@ -5065,7 +5096,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.requestHistoryAroundVisiblePosition(synchronous: false, reloadAtTop: true) } - private func setStoryFolder(id: Int64?, assumeEmpty: Bool) { + private func setStoryFolder(id: Int64?, assumeEmpty: Bool, animated: Bool = true) { if let listSource = self.listSource as? PeerStoryListContext, listSource.folderId == id { return } @@ -5073,8 +5104,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if let id { if let cachedListSource = self.cachedListSources[AnyHashable(id)] { self.listSource = cachedListSource - } else { - let listSource = PeerStoryListContext(account: self.context.account, peerId: self.context.account.peerId, isArchived: false, folderId: id) + } 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 } @@ -5082,7 +5113,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.listSource = self.defaultListSource } - self.requestHistoryAroundVisiblePosition(synchronous: false, reloadAtTop: true) + self.requestHistoryAroundVisiblePosition(synchronous: false, reloadAtTop: true, animated: animated) } public func beginReordering() { @@ -5156,6 +5187,58 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr 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 { 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/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 6d1ece1b61..3b86700ea0 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 b8eb922eb4..8c71fbe8ec 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/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)))) From edf2cfefc915d89c4a2b158e67fa8a0e873b16de Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 22 Jul 2025 16:25:15 +0200 Subject: [PATCH 06/11] Add items while creating a folder --- .../Messages/StoryListContext.swift | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index 6479c83182..3bbe83849f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -1388,7 +1388,7 @@ public final class PeerStoryListContext: StoryListContext { }) } - func addFolder(title: String, completion: @escaping (Int64?) -> Void) -> Disposable { + func addFolder(title: String, items: [EngineStoryItem], completion: @escaping (Int64?) -> Void) -> Disposable { let peerId = self.peerId return (self.account.postbox.transaction { transaction -> Api.InputPeer? in return transaction.getPeer(peerId).flatMap(apiInputPeer) @@ -1398,7 +1398,7 @@ public final class PeerStoryListContext: StoryListContext { return .single(nil) } return self.account.network.request(Api.functions.stories.createAlbum( - peer: inputPeer, title: title, stories: [])) + peer: inputPeer, title: title, stories: items.map(\.id))) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) @@ -1428,6 +1428,18 @@ public final class PeerStoryListContext: StoryListContext { 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)) }) @@ -1787,10 +1799,10 @@ public final class PeerStoryListContext: StoryListContext { } } - public func addFolder(title: String, completion: @escaping (Int64?) -> Void) -> Disposable { + public func addFolder(title: String, items: [EngineStoryItem], completion: @escaping (Int64?) -> Void) -> Disposable { let disposable = MetaDisposable() self.impl.with { impl in - disposable.set(impl.addFolder(title: title, completion: completion)) + disposable.set(impl.addFolder(title: title, items: items, completion: completion)) } return disposable } From bc49c5e78823230dfc2efd0d953e672840266f79 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 22 Jul 2025 17:24:33 +0200 Subject: [PATCH 07/11] Update --- .../Sources/AccountContext.swift | 2 +- .../Sources/CreateGiveawayController.swift | 4 +- .../Messages/StoryListContext.swift | 46 ++++++++++++++++++ .../Sources/MediaEditorDraft.swift | 8 ++- .../Sources/EditStories.swift | 2 +- .../Sources/MediaEditorScreen.swift | 40 ++++++++------- .../Sources/PeerInfoScreen.swift | 2 +- .../Sources/PeerInfoStoryPaneNode.swift | 44 +++++++++++++++-- .../Sources/ShareWithPeersScreen.swift | 40 ++++++++------- .../Sources/StoryContainerScreen.swift | 2 +- .../StoryItemSetContainerComponent.swift | 16 +++--- .../AddFolder.imageset/Contents.json | 12 +++++ .../AddFolder.imageset/addfolder_24.pdf | Bin 0 -> 5157 bytes .../AddToFolder.imageset/Contents.json | 12 +++++ .../AddToFolder.imageset/folder1_24.pdf | Bin 0 -> 5273 bytes .../TelegramUI/Sources/OpenResolvedUrl.swift | 2 +- .../Sources/SharedAccountContext.swift | 2 +- .../Sources/TelegramRootController.swift | 8 +-- .../WebUI/Sources/WebAppController.swift | 2 +- 19 files changed, 186 insertions(+), 58 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddFolder.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddFolder.imageset/addfolder_24.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddToFolder.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddToFolder.imageset/folder1_24.pdf diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 2df656d83a..cf314b5f76 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -886,7 +886,7 @@ public protocol MediaEditorScreenResult { public protocol TelegramRootControllerInterface: NavigationController { @discardableResult func openStoryCamera(customTarget: Stories.PendingTarget?, transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? - func proceedWithStoryUpload(target: Stories.PendingTarget, results: [MediaEditorScreenResult], existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, folders: [Int64], externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void) + func proceedWithStoryUpload(target: Stories.PendingTarget, results: [MediaEditorScreenResult], existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void) func getContactsController() -> ViewController? func getChatsController() -> ViewController? 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/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index 3bbe83849f..1500524266 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -1486,6 +1486,44 @@ public final class PeerStoryListContext: StoryListContext { }).start() } + 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] = [] @@ -1815,6 +1853,14 @@ public final class PeerStoryListContext: StoryListContext { return disposable } + public func renameFolder(id: Int64, title: String) -> Disposable { + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.renameFolder(id: id, title: title)) + } + return disposable + } + public func reorderFolders(ids: [Int64]) -> Disposable { let disposable = MetaDisposable() self.impl.with { impl in 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/EditStories.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift index 3359bf0c1d..e8cc73e516 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift @@ -163,7 +163,7 @@ public extension MediaEditorScreenImpl { } else { existingMedia = storyItem.media } - rootController.proceedWithStoryUpload(target: target, results: [result as! MediaEditorScreenResult], existingMedia: existingMedia, forwardInfo: forwardInfo, folders: [], externalState: externalState, commit: commit) + rootController.proceedWithStoryUpload(target: target, results: [result as! MediaEditorScreenResult], existingMedia: existingMedia, forwardInfo: forwardInfo, externalState: externalState, commit: commit) } }) } else { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index f4bc125538..f013889619 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/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 1c56d9d1af..a2b2b6ddc5 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -10260,7 +10260,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro viewControllers = viewControllers.filter { !($0 is AttachmentController)} rootController.setViewControllers(viewControllers, animated: false) - rootController.proceedWithStoryUpload(target: target, results: [result], existingMedia: nil, forwardInfo: nil, folders: [], externalState: externalState, commit: commit) + rootController.proceedWithStoryUpload(target: target, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) } }, cancelled: {} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 41196da9f2..42518d7176 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -2491,7 +2491,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr }))) } else { //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 @@ -2512,7 +2512,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr }))) 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 @@ -3883,6 +3883,18 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.beginReordering() }))) + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Rename Album", textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.destructiveColor) }, 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: "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 { @@ -4931,7 +4943,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } if let value { if let listSource = self.listSource as? PeerStoryListContext { - let _ = listSource.addFolder(title: value, completion: { [weak self] id in + 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 @@ -4949,6 +4961,32 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr 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).start() + } + } + } + ) + self.parentController?.present(promptController, in: .window(.root)) + } + private func presentAddStoriesToFolder() { guard case let .peer(peerId, _, _) = self.scope else { return diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index 5cb946e270..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 @@ -1421,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() @@ -1429,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() @@ -1457,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() @@ -1545,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() @@ -2267,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() @@ -3019,6 +3024,7 @@ final class ShareWithPeersScreenComponent: Component { self.selectedOptions.contains(.screenshot), self.selectedOptions.contains(.pin), peers.values.compactMap { $0 }, + self.shareToFolders.map(\.id), true ) @@ -3270,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/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index df2e7e0a69..1b7327eb71 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -1695,7 +1695,7 @@ private final class StoryContainerScreenComponent: Component { if let content = component.content as? PeerStoryListContentContextImpl { if let listSource = content.listContext as? PeerStoryListContext { - let _ = 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, let id else { return 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 0000000000000000000000000000000000000000..e5434739fdb16d838f625d54698eeb610d84091f GIT binary patch literal 5157 zcmai2XH-+`(kApO9i)e5K!Ff?Xwr-H-o+522?C)6hzQaJrHCNCSCJM$LAnAeASgw8 z4T1#_l->e-;5nXq&OLXn@BOj&tY_YN_RQ=b^Q;jz)zT4%NXSzQhn^m42v8D;c5$Ny z0+p44V0Cv4!p9SaK>)#82!9kDVXmuj>N-7O9n_ic^zDA?)R!l{ zLmrVX53_7EQelIe zeOcZd61gf;DjjAlSE6>r-brN;WcNIKrL4mo?TL^o(QOS0|yE}M;6=}5V464 zIA7ZwJDt%*cX$qC`I>8ojmfU{#PqP01xuinsLf{Yn6_P9pv+TCXXE1R(`eu!QidKY zu?2Uvu<+6obyx|7+#ooLL-ee(#Pk_~eaYU`dC6g3avcXX`Y4ykO9=15pkowA?EW-q zWHaN18&~%9c+BL|cwN{((KkIG7qlg`|e zn>`;~bGIH41M_IqHpQ#89zppudp*yqX&J|n4~Mc1kh~2AxQCK>k*~et+KW(QpcZI% zdqqYCB#TjF8z-s{S2iWu2|dc^<_5^-pOXod&L?{w5sRm80C+|KBx#GU(YBCU;Ca%+ zfRq$wz>CG?C{6N24p$NgzfCunJu#W)LsME7dMbXZ=YUCaR<$>#P)o8A^~;)H^J52O z%jlcv^{8sGiP8E^ZK` z^xkwp-}x^*Y_V4DoG_lJcgFSH1^iX&8E#Wf#jL*Gf$7$KRNywIyK&dIo$Ql>0z`s7 zfFXc705JA|t26jj^3O>uR|~x=HTd;cyumtXooDY3!Iwa*9(GO5OMsLK8(9}Apg~s< z?-eVllg@vUDLzKxwW*7M_obS+akUo1AiW?>frl9VgyPRa02IxWI7}ucMMmR5&srcW z$!-x@8`+W-ls&EAW^zZfz?4>W?UBg+^x7lxKVu4Si_Wsnb!L8Q z+sT|x?;XVTJ#hB1lY)NFo1Pr_Y>RDoE~kxv*oW8@!HW`o&e-eoBy_f&mV&nM*0+%O zEY6Ra_L*&&s7wl3Fv{5N39|8~uv-e!4{3z7sB@|#n-rd8f9^bxvc$XP8pzR_rCL%{ zu<%%L@X659NXDS-kW)cc(Nx)?-h;BP;nyn%U`O$ym}G_Xb(jtUh{gD z--T~2cnE8lKiA3D=`Cm_SU;#(6j&7fmIW@qfLU-`Xue>}T&kbc!=F4=`t(T-DjXHH zBrZMPH*0v!zq_)Z8@`5=kip6Exg>J%#u@6Uz+Y>;ak;0RpdI_>l0k5YpTac1jaUeqJ5LLL11OTS_QIlTce82#K;ghV>)T60mzp{jI}1CPJ7%MAN3lm6 z2#N{H=}gC1K48(R*O@QuD0C|-wF$&czw{BClu!f*dZi4Of43U5uD}^z4Zagk;QWyJ z43eJyGd&rS2Mx=1`+yNss+aP=-}{Ld-?rGec?HWH#C`{Iax2@w(R&)(VnAryyGQyW zawt+CU|=eN&H0vw6#3<}7mNa~ z%;l*HQ!|WQSL3LVvVSA9sOw|lA~qqx(Svzk*-c1fP4$y=fHOt% z^vxtGHO&Ov4$gJV6+t7THxmSv29?N^ZU=}@`gx6w1zkPfCawq{3h$4a?pV2Odt$oN z`F*Wnrd4}KJ9_upF6A!mn%~szLfaaw)B3lzlEX}cZ^HY+=v~}INY!xtRyW`ipa+1O zNU9x;IH1_&COGDrs+!K*4cb02YpmEf;-4)z?p%xJNF^u5qtsP>1d+)n1jkmW?uj&_*X4)wy}6GdG2nk&GrJ) za97tGeV%j`eeJMHt5_Y&uFh%x&@$`Od91T()SPAXa^h%o_~zVHQ186;ibvO!M{w++ z;=#-Tcy_(X_p`*|T6`FSBsk&{`42j6di@{kYw>d?Z6^onUm`N*#pxmRn|z|m7lSL; z8xHREB5ZN93t-u{zhrAm);bmR$lq`?T(Vo-0=8WBazG2nu zy>7zzrYPa#W=3UyLqEdVuD_haBzACicq-g8FP9 z5g!s#GP5r-4r&f5II8BTr)8v^WSQJkp;YVjC$i)Vish%y4C%XF3suMGk%ia*p%*Jgn@BvwUa^)Q3D4 zG#)w^8rPsL=WC$u`dSd4+xMXjLP}&q{3@+*Vj`n_VglSPX4*-FxpGVvPdO+^jqr5+ zQxTq8XR4E0>M!~EOFsUFrT>B*fM9)1O%0eY!WDR?Rn37;XEeWpW&XN#rd>7B?r0wi zFBly0OUi0OfM+b`r!w?RiT;j}{YT9ID{AG=DE_Hs0r9Eo3Ec9u+PlH`o77y*3Ps#j znMt7?L}Bc3lCU52i@{cYMz>q^I7uA#gw2P)g5H3gSN9Ia7HCddkAeuBPtA9Kl=Z*i z+g&~$z4D{24>Z^Jr*R#OE8+tSv_OxP@LgaV zR_ZIWRd%p_we1mNQpg*RQ=G6_XV(8vGwTkXLvnqo57_wVId%|PkBNs{!! zI7l9o*JPx=$H+&OELxELP><~;yhx!1Sj28vY(lTcUdvFM=0;N;`8vE74tk2qN%&M2 zt;-t`_Gs|8W)vQY%|7N@9wIB6zl&1&tJVgXKeTXHJaS!W>B_=`hwr`@J)Y(6utBo+fXMn*r1vyiC zTSwIi6G-_h*xr?x*@4FKrG-3{LRCVUTZI)g_A+G#;Hf#fLK<13{7~4IzF-y$Ui3p9 zj>z@dw$TI+HSmU4bb#U%>n6?H_blzbZN?UbN*)18FKvBs318He$ulRXtF7iK-A6do z$&s8wjITxL1a2ziM;S46F;j-I%I!1AMR#^xP2_$!=rKI1rZA_Uz`ciOLSuZY=gK3X zj$XVJ6|4Sb%wy|pw*;JV-&p@UQG&2@)>i!qSJV>A;}_4SCF&5``CzrUjZl3UZ3`<#@ls2Q}e%qjj=&^by390_mZY8&{?n zh6Qewg(i+|@>f2pdKdbv$)c32IOUno0qudHqPoNU-X7s@n@U?j9mr5Nr{f@DTp84Z z1wdVy=tlTseu4%SW{c2?Qc5j5WE9(p zbx-2c`58GfvyfVeP=U{Ij`v!EjprS?-Ab$<;f9Ed1J=9RjKLZ@YHZ5)75LdJm>HLH z8LlWLiR$MnhFf9$k#lw*%pbt7d;<@Z)b?89U1~T=;w8Oav8a5PMz-H-(NVsmNP;|A z{DiF>j*g^y8=geI&@-m2`&G zR*}m@PPg9EsLwC(cvcf?3qMyd)-*kR(bYxj6~Sp&JD9SLjiBX-an!tfu3&pb1#y1@ zOxqj||NMX=R_oh7P?RCu^#V&#DGxyOD}9V^1cxERc4Hab$i%qgHJxYzQq&^YQkAtu z+U93w^@tc1>4;VCImmgT`t4`d98}2YX#q;2dVEzHl%t&(URd;)#(WH~O|{*`A7;|77=ig4o87%e#b3xYi{_5E>!^5tRAV$dM6!D(wNMqKCo|J z!7jAcrgD6TfF68IaLFVBq!TkNW|YUc=j#5W zBW|6HeQbxhJ*||zzJC)jd)a>-SC2NaJc#-Zf#aQ3HJaUktor3ocL`? ztEtWamKjO#0aqh@U}^QzZMilx?9KIa-3E^GrBBF0`HG#%!Q<8s2zm4P6GOh#00sGt zf|C$H!0@}r|6^31jiNs`H4r&zg}(-x{9o{|1&*gD8siIeaQbr-V|mN#beCfayYYKj zV}x);!8FiVpaW1+Lh{%7k0B-V8~Ts+o5>vEi}v$@BYc5pWA|((o^6FR(ViHD=jj*E z=IU?I6y|e^0Tup){x*Hjpg(FjOV$A8dYUTmtb((#{+rS7G#2u&{C!W6zcE4o!E{!j z?_VE0OYD!rHDMT-JNm{yL$N-H>(r7!h$QtN^`AX~a~4#zg^DR`#fvy-?KU4%Bq4;AxRKt;Q0bU!NOpawF3wY zmX!rV6r9iqcNZ%(0t`_`cp+^NdbbqMRp$$$ioDRBzn#w2$_N{jE#hyDCc@=D+8!(- zE+P3hq74>3m*nI?9%y%jl`{yFUXzG~Q%}%#qrCT`1pxr#_LeZMM7*jn!K_DtPB~ZN z@T@LTUTHO>(hZ_gQ6@B%CsJW{MH|^tQt3_;0f7dlkH|ppq@p}%R372hrY z^romW!2etN)KbQA_8A`5g+N)@J&&X-$I?Q_N7RkYvgy(=svkDb}&z$-;ih z(E4{b(XdkBwcoiOaR0C%6GwG2EZ|en@USrk%Qs| z6b%&7^3;PCgWUAMcq_(|OHA#F!qj_#1ha6=Rc4C(j@4OHjHD^i-H!ZOyMt?f1gwF1 z8yXl!ApH6>&~4%_d8n>{0hkYTHT+7pMD~_$UV(!FK@s^_a6@oh4wo+5kdfF`ty`jl z{q{F^Kw@;yV?KolDJSLXWTg$wEXyoY?a&3C-C0M$zF zO$PMQe`TSKywk>L#ZvL`qngt-uN$?Lz9dr--#hoLZdHy*F>90G5A$dP&Pz!_g($o! zy+Pi9@uy53{;lyFlNhEZ3ONv@b7@v%lY5ip;NgkK37JBWrM&Ak!Yi1NnviRX4IY%&s{8Q;Jlj>|B_eL-@LQ2|D8Gj+c1 zigpjY$K-MTvtqR7O58BdRh6;assht*5)S0MBA!uUc??4ztCjBozH{G$INvr_pD=`J z!c6KF51XnCfO*yh?S|@xyn3))k%BE`&{zDYz!P92t(d^F#fG_Oc2Ogrg}6nHuBZHN zJfqor6G3(7@_Uh!9G=w;g-7nY0=pgGlE%2{hH0fCi$Z-C7`r(Fa-$9d4kMf9x6tS` z#*x%}sV%9zTT13A;I3$Udvv;#;gW7$vH_^Z84az!n$MY!_b^2mzSHp zkk2txIJ`2JG9*51mYtS6RdB5Kw4i%5a)f_qbvSK!{DVc&8-+LKXF-6rKqS`EwKc1I zwmCc2s&4Lb2W^KN#~qH^A?aM7-0-(FHj)eI1&4)3E+guf>Tx~n@l!7=3ZEk%AwyRL zML+g^(zNu#l?*W3tlJBTl}oT$V;NYZG*xfdbSidPKURrRiR|Ln@bBg6ZBD$AXi>UW z5?AU_5>}c}a$4$9GF5U`1+AHzH264erfj_U%6>cNgG#ycd#A(NaOWQry5AEgzgMhQ z9b5mfgjAs_GF=C~Bistm^wz`43Vn)S@7#*Y&0nzV_UeABy{B!RFRC3NEt(#mt|R>v zB5|+J!t@npRAoGLLGQ~o&%DOBb*!EV4J&nS(?v5G4XgFr(~dKlzR8&DKIvPSFUT*v zTTWYk1Mb$&ww^xOO@7T=%|6WuO?NR#zBl)Vb|3E-?x$?kY@7XEtr3e4BgOo~XX77R@~g3P?$nG*>{v?BzAz*v?#$c8Rf7w$7y z-douloOo9tfYynniM@_Hgt3Zi(H7ST9~jIToV)s#|2ls|`(j5<$G7%R;l81C;TjwQ z91^P25e83blxtPza@unoa$nx{DW87hE-)!14e@bJ7%E!3Gj3R1u5IrBF#3eCKlK$f zIe8;F9-0XYN_Xf-3&_-pcs=QzXPs?Xtlz$kq4uMD2t9j{u3_pnjcL+2X*qaI_&Q`b zL>-_37s5PWPQqN@^w+Z--h45!xUu+_#_zV~J6Lv{u=>a6MG2|TCzI?F6O*{Kyl;Qb zl;Z?Zj@ijG6pP&J5?j3Gu5T?cA;i#weqVw+!Cp=DlrVvsB>Q`mu1uOaW|nUo?=Sf5 z_^cn5kxMhxy3kK7S5`VuSMz%5kJ~lBuTSf)J#c|lCe(=|S5YuSSQiZSIuzyDJJvT7 zC(|(F?dzXmn!$sz58saAkQtHz%J_N@@rqLOEuVZb! zZl+mfPbGZca-U?MY~6FpH^-Jlo&yMwQrWkuxUa75>g4yL}zRyKp9Xh@Z+kCW1 z>CPAn8QVF*om7??HrHnD^}5ZutvsHkp7Ffw>B%jwO*_`H8+Y_re<c+ggojT5l#!Ka4tlp7QIRGhB71n!2b5`JoIQf=zbLOlGb`>hVcW=jMue@E-0TuJ6#(zqjMeOvz>|JBZ{vq3_T>1g- za9%bL~h1gTl2l$oXJ8EmNpbc19sHH+Vv*J?Ntgy zy-3k+SR!^CdW*wzEcW$ic>;4T&1XaKe@taDt_%wz(<_;F8*{KDcvU%Ba*Blo5A;rN zpU7H!VoRP-bs~pM^U8B3#h`-@TH|}|sozU&4G!^FMsA_!4V3jtDp)6^5#+0*HL)3p zw3vdyNxF8qiT7C_{em+owrwUW+{$Vda;a$j$HLTFA09yqLlgtek1M2P?+pOzUdv?4 zBZoeO6%e+~`6NRiUgl4?r^$S)I`YyQVaJ8B?gve2o@Y&mLPRuCh%20ji3qt7~lYjH-C} z#SCf#V6U_M@hH6|u7pL-ca@Hy;uYv6eW39xT)J$YP9pS^q2?#WEu#sl+)8b=er0dN z{DqRs4e#RjPwD-&)pGKqTfk%)a5Skj#)j>wXO@h9K5AoI_ZG~|wu?#Nak)D2?MGGI z*b&G|V7V$vfA#89X=dY{6x5sqs zuP^laRQKc%JyR;%SLe|aHBdIsqUyO;{l00DP(GnIujj=n0Kzn?^Z5&|uu{UF{y5C3;>JmhZ%})!LNRtJ~N_&WyY_V8I*)cx;GQvp@WIrS{<(EXrGVXZ7N7K!RAScEXN_%Y zCFOXV>ge1!hnTR~=h)?7@;KceEVtPs0;pCaD(DUI&9P`!*icm#KaE29gMN^WueI26 z=iX?B@rqx#-Qh@04Rtnu*HYsn8SN)CA2H=;yky-k?rYLMX%o2_%N;q&BRtBsOhY*4 zY&*mv8&u~}3PB8WRmW&r1+%~XN}oQlsm3mKy*#-+CP|EPUt`2+sbte!L)^NO+^41b z%iHxm|F(W1I$}PruFGzeU?kwq$x}NyY+R+F_#2!%Z&X?*xw|x)3lHc_vU>hk+aESo zaW;QoE>A|8S((#{$NEiadvOpTQcK~fsr{6FJh-zzPwCXrlyZ)aqT{Bxg|JndZ-5lgB;zp>TRkB;Dl(OiSsKwH zS@2RX4)U?lm#*s>%k>y0T}x*N*z~Z!@0Sd5-K$st-(9|>jx;4UBXA39^Ty!QB~i~m zVJ|jDP{{xknV4hUX_tPMZUeDK*OEm^J|NY}jYy+eHpZ+>zKj@ljL|f>3cVv*#9Dh* za1%!Kk}@)9O*cb{-sP{#8@$&F9#k)&m>Efil|?Ay3hI{Io8(os8}Rr>gb8{$=r^Pf zP-+|yo_eO0xJQjL3$72loyisJ^X9-yMkCC6q}F`sCVWmoIVJUpWQm|=W{1dE6Kg<) z{F5C|$tLzNQjR*!ZB-a>!v9EOnl!X0uxGTy$;mB?WZ^UQ_*6flg64gyX&F~;H&@DG zrB^YGIvm{$jEU>wQk|r?ZBZ(l#w^2zDrVwWli)(wX_j5iqw%E3XvA>5r1I_(%&p61 zgH^+f>!;s|-^tGd;z{`l-T%>^FWTQ<(;=v&xY(a2D)|TgH8gT@L7_dsCT4$6e+(YD zp3jfqR`-7oh_n#4NGnAY25bTr783ro{w0Wr{f7P}{f?xE@IZOG+aNr^7p?xH4_{28 zlu#~cgv;d6@gpP0!1u#-4vJO5!P_22QtB*g#APE11b{GR{mD<&m&p6!2NFp+=r zMY~%eoe=Jzi!VT3`Mjh&9w=u7FPH~u%llVhA$lkj8hlZ-UzY^YNBSTxD)4JAXyuOn RRTD9oln98EQ$ Void) -> Void) { + public func proceedWithStoryUpload(target: Stories.PendingTarget, results: [MediaEditorScreenResult], existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void) { guard let results = results as? [MediaEditorScreenImpl.Result] else { return } @@ -582,6 +582,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}) { diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index aaa520fd2d..3445a10613 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -1494,7 +1494,7 @@ public final class WebAppController: ViewController, AttachmentContainable { externalState.storyTarget = target if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { - rootController.proceedWithStoryUpload(target: target, results: results, existingMedia: nil, forwardInfo: nil, folders: [], externalState: externalState, commit: commit) + rootController.proceedWithStoryUpload(target: target, results: results, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) } }) if let navigationController = self.controller?.getNavigationController() { From 9c4749625308a992aa001e69c08b832793a7aa0a Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 22 Jul 2025 17:35:00 +0200 Subject: [PATCH 08/11] Fix --- .../Sources/PeerInfoStoryPaneNode.swift | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 42518d7176..c3437605e5 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -2937,6 +2937,9 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr 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 @@ -3856,6 +3859,18 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr }))) } + //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 { @@ -3883,18 +3898,6 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.beginReordering() }))) - //TODO:localize - items.append(.action(ContextMenuActionItem(text: "Rename Album", textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.destructiveColor) }, 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: "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 { @@ -4979,7 +4982,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } if let value { if let listSource = self.listSource as? PeerStoryListContext { - let _ = listSource.renameFolder(id: id, title: value).start() + let _ = listSource.renameFolder(id: id, title: value) } } } From 0c9b15b18115cd71a3ca2a1d7c2a9599e449b7d4 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 22 Jul 2025 18:24:46 +0200 Subject: [PATCH 09/11] Fix menu --- .../Sources/PeerInfoStoryPaneNode.swift | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index c3437605e5..2583e67945 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -3859,29 +3859,31 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr }))) } - //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 - } + 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) + }))) - 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) - }))) + //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 From 46dad2eddc2c360ca33d57c9c2b6ec50da3fea9d Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 25 Jul 2025 15:20:36 +0200 Subject: [PATCH 10/11] WIP --- MODULE.bazel.lock | 19 +- .../Sources/AccountContext.swift | 19 +- submodules/ChatListUI/BUILD | 1 + .../Sources/ChatListSearchContainerNode.swift | 6 +- .../ChatListSearchFiltersContainerNode.swift | 50 +- .../Sources/ChatListSearchListPaneNode.swift | 906 ++++++++++++++++-- .../ChatListSearchPaneContainerNode.swift | 9 + submodules/TelegramApi/Sources/Api39.swift | 11 +- .../Contacts/TelegramEngineContacts.swift | 2 + .../Messages/SearchMessages.swift | 84 +- .../TelegramEngine/Peers/SearchPeers.swift | 4 + .../Sources/ButtonComponent.swift | 2 + 12 files changed, 1001 insertions(+), 112 deletions(-) diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 169cf0ff99..cf536336c5 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -10,6 +10,8 @@ "https://bcr.bazel.build/modules/abseil-cpp/20230802.1/MODULE.bazel": "fa92e2eb41a04df73cdabeec37107316f7e5272650f81d6cc096418fe647b915", "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed", "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/source.json": "9be551b8d4e3ef76875c0d744b5d6a504a27e3ae67bc6b28f46415fd2d2957da", + "https://bcr.bazel.build/modules/aexml/4.7.0/MODULE.bazel": "4030ff1555ade0956c08c74722851fcca0dc02ec7b8e7c61d0bbc4806ec4b2de", + "https://bcr.bazel.build/modules/aexml/4.7.0/source.json": "641c9de95dc10b8bf3685b5de9f9a84e7470ec3e40a09d7ddc9e3c9f2289a931", "https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd", "https://bcr.bazel.build/modules/bazel_features/1.10.0/MODULE.bazel": "f75e8807570484a99be90abcd52b5e1f390362c258bcb73106f4544957a48101", "https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8", @@ -48,6 +50,8 @@ "https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902", "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/MODULE.bazel": "6f7b417dcc794d9add9e556673ad25cb3ba835224290f4f848f8e2db1e1fca74", "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/source.json": "f448c6e8963fdfa7eb831457df83ad63d3d6355018f6574fb017e8169deb43a9", + "https://bcr.bazel.build/modules/pathkit/1.0.1/MODULE.bazel": "fae93989a10f8d90d5ac02453e6632ae7f71111687862c01f468858cef40bb5e", + "https://bcr.bazel.build/modules/pathkit/1.0.1/source.json": "3215e6b4b08f96f34024eaf186d247744ca255925d7ee3f50cf94f7cf885696b", "https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5", "https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f", "https://bcr.bazel.build/modules/platforms/0.0.11/source.json": "f7e188b79ebedebfe75e9e1d098b8845226c7992b307e28e1496f23112e8fc29", @@ -144,6 +148,8 @@ "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91", "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/source.json": "32bd87e5f4d7acc57c5b2ff7c325ae3061d5e242c0c4c214ae87e0f1c13e54cb", "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", + "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", @@ -284,7 +290,7 @@ }, "@@rules_xcodeproj+//xcodeproj:extensions.bzl%internal": { "general": { - "bzlTransitiveDigest": "m7EcAC1RuIDErOmw2fxoQ7OGzFPZJtVcXgIIqZMO3OI=", + "bzlTransitiveDigest": "+kmqZtEKFY8zgqpV6mrwdQkTJqGUZhL8b3ZMsxrqSyc=", "usagesDigest": "fvsnMonVwKDYnBfww4bXuYie3WU0d9VSqT2gePSdQco=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -306,7 +312,7 @@ }, "@@rules_xcodeproj+//xcodeproj:extensions.bzl%non_module_deps": { "general": { - "bzlTransitiveDigest": "m7EcAC1RuIDErOmw2fxoQ7OGzFPZJtVcXgIIqZMO3OI=", + "bzlTransitiveDigest": "+kmqZtEKFY8zgqpV6mrwdQkTJqGUZhL8b3ZMsxrqSyc=", "usagesDigest": "jzxYhnOC9BE0dJ0biFLfxWXi/+R19uAAZkJ0p9CY0JI=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -339,6 +345,15 @@ "url": "https://github.com/apple/swift-argument-parser/archive/refs/tags/1.2.3.tar.gz" } }, + "com_github_tadija_aexml": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "build_file_content": "load(\"@build_bazel_rules_swift//swift:swift.bzl\", \"swift_library\")\n\nswift_library(\n name = \"AEXML\",\n srcs = glob([\"Sources/AEXML/**/*.swift\"]),\n visibility = [\"//visibility:public\"],\n)\n", + "sha256": "5a76c28e4fa9dcc1cbfb87a8518652628e990e522ecfbc98bdad17eabf4631d5", + "strip_prefix": "AEXML-4.6.1", + "url": "https://github.com/tadija/AEXML/archive/refs/tags/4.6.1.tar.gz" + } + }, "com_github_michaeleisel_jjliso8601dateformatter": { "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", "attributes": { diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index bdbab5249c..1d3ab0132f 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -719,6 +719,7 @@ public enum ChatListSearchFilter: Equatable { case topics case channels case apps + case globalPosts case media case downloads case links @@ -740,22 +741,24 @@ public enum ChatListSearchFilter: Equatable { return 2 case .apps: return 3 - case .media: + case .globalPosts: return 4 - case .downloads: + case .media: return 5 - case .links: + case .downloads: return 6 - case .files: + case .links: return 7 - case .music: + case .files: return 8 - case .voice: + case .music: return 9 - case .instantVideo: + case .voice: return 10 - case .publicPosts: + case .instantVideo: return 11 + case .publicPosts: + return 12 case let .peer(peerId, _, _, _): return peerId.id._internalGetInt64Value() case let .date(_, date, _): diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index 808dfa9b0d..9d17f95213 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -115,6 +115,7 @@ swift_library( "//submodules/TelegramUI/Components/AvatarUploadToastScreen", "//submodules/TelegramUI/Components/Ads/AdsInfoScreen", "//submodules/TelegramUI/Components/Ads/AdsReportScreen", + "//submodules/TelegramUI/Components/ButtonComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index e7c445e693..9d90ac12a8 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -345,6 +345,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo key = .channels case .apps: key = .apps + case .globalPosts: + key = .globalPosts case .media: key = .media case .downloads: @@ -388,7 +390,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo switch filter { case let .filter(filter): switch filter { - case .downloads, .channels, .apps: + case .downloads, .channels, .apps, .globalPosts: return false default: return true @@ -675,6 +677,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo filterKey = .channels case .apps: filterKey = .apps + case .globalPosts: + filterKey = .globalPosts case .media: filterKey = .media case .downloads: diff --git a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift index 05495508a2..d26010892c 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift @@ -13,6 +13,7 @@ private final class ItemNode: ASDisplayNode { private let iconNode: ASImageNode private let titleNode: ImmediateTextNode private let titleActiveNode: ImmediateTextNode + private var titleBadgeView: UIImageView? private let buttonNode: HighlightTrackingButtonNode private var selectionFraction: CGFloat = 0.0 @@ -74,6 +75,7 @@ private final class ItemNode: ASDisplayNode { self.selectionFraction = selectionFraction let title: String + var titleBadge: String? let icon: UIImage? let color = presentationData.theme.list.itemSecondaryTextColor @@ -90,6 +92,11 @@ private final class ItemNode: ASDisplayNode { case .apps: title = presentationData.strings.ChatList_Search_FilterApps icon = nil + case .globalPosts: + //TODO:localize + title = "Posts" + titleBadge = "NEW" + icon = nil case .media: title = presentationData.strings.ChatList_Search_FilterMedia icon = nil @@ -133,6 +140,38 @@ private final class ItemNode: ASDisplayNode { self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: color) self.titleActiveNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: presentationData.theme.list.itemAccentColor) + if let titleBadge { + let titleBadgeView: UIImageView + if let current = self.titleBadgeView { + titleBadgeView = current + } else { + titleBadgeView = UIImageView() + self.titleBadgeView = titleBadgeView + self.view.addSubview(titleBadgeView) + + let labelText = NSAttributedString(string: titleBadge, font: Font.medium(11.0), textColor: presentationData.theme.list.itemCheckColors.foregroundColor) + let labelBounds = labelText.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: [.usesLineFragmentOrigin], context: nil) + let labelSize = CGSize(width: ceil(labelBounds.width), height: ceil(labelBounds.height)) + let badgeSize = CGSize(width: labelSize.width + 8.0, height: labelSize.height + 2.0 + 1.0) + titleBadgeView.image = generateImage(badgeSize, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + let rect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height - UIScreenPixel * 2.0)) + + context.addPath(UIBezierPath(roundedRect: rect, cornerRadius: 5.0).cgPath) + context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor) + context.fillPath() + + UIGraphicsPushContext(context) + labelText.draw(at: CGPoint(x: 4.0, y: 1.0 + UIScreenPixel)) + UIGraphicsPopContext() + }) + } + } else if let titleBadgeView = self.titleBadgeView { + self.titleBadgeView = nil + titleBadgeView.removeFromSuperview() + } + let selectionAlpha: CGFloat = selectionFraction * selectionFraction let deselectionAlpha: CGFloat = 1.0// - selectionFraction transition.updateAlpha(node: self.titleNode, alpha: deselectionAlpha) @@ -163,8 +202,15 @@ private final class ItemNode: ASDisplayNode { let titleFrame = CGRect(origin: CGPoint(x: -self.titleNode.insets.left + iconInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize) self.titleNode.frame = titleFrame self.titleActiveNode.frame = titleFrame - - return titleSize.width - self.titleNode.insets.left - self.titleNode.insets.right + iconInset + + var width = titleSize.width - self.titleNode.insets.left - self.titleNode.insets.right + iconInset + + if let titleBadgeView = self.titleBadgeView, let image = titleBadgeView.image { + width += 4.0 + image.size.width + titleBadgeView.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: titleFrame.minY + floorToScreenPixels((titleFrame.height - image.size.height) * 0.5) + 1.0), size: image.size) + } + + return width } func updateArea(size: CGSize, sideInset: CGFloat, transition: ContainedViewLayoutTransition) { diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index cb4f254746..24e109925c 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -33,6 +33,10 @@ import FetchManagerImpl import AnimationCache import MultiAnimationRenderer import AvatarNode +import ComponentFlow +import MultilineTextComponent +import ButtonComponent +import BundleIconComponent private enum ChatListRecentEntryStableId: Hashable { case topPeers @@ -285,6 +289,9 @@ private enum ChatListRecentEntry: Comparable, Identifiable { header = ChatListSearchItemHeader(type: .text(presentationData.strings.ChatList_Search_SectionRecentApps, 0), theme: theme, strings: strings, actionTitle: nil, action: nil) } } + } else if case .globalPosts = key { + //TODO:localize + header = ChatListSearchItemHeader(type: .text("Public Posts", 0), theme: theme, strings: strings, actionTitle: nil, action: nil) } else { header = ChatListSearchItemHeader(type: .recentPeers, theme: theme, strings: strings, actionTitle: strings.WebSearch_RecentSectionClear, action: { _ in clearRecentlySearchedPeers() @@ -1111,23 +1118,30 @@ public enum ChatListSearchEntry: Comparable, Identifiable { } } else { var headerType: ChatListSearchItemHeaderType = .messages(location: nil) - if case let .forum(peerId) = location, let peer = peer.peer, peer.id == peerId { - headerType = .messages(location: peer.compactDisplayTitle) - } var actionTitle: String? - if case .generic = section { - let filterTitle: String - switch searchScope { - case .everywhere: - filterTitle = presentationData.strings.ChatList_Search_Messages_AllChats - case .channels: - filterTitle = presentationData.strings.ChatList_Search_Messages_Channels - case .groups: - filterTitle = presentationData.strings.ChatList_Search_Messages_GroupChats - case .privateChats: - filterTitle = presentationData.strings.ChatList_Search_Messages_PrivateChats + if key == .globalPosts { + headerType = .text("Public Posts", 0) + } else { + if case let .forum(peerId) = location, let peer = peer.peer, peer.id == peerId { + headerType = .messages(location: peer.compactDisplayTitle) + } + if case .generic = section { + let filterTitle: String + switch searchScope { + case .everywhere: + filterTitle = presentationData.strings.ChatList_Search_Messages_AllChats + case .channels: + filterTitle = presentationData.strings.ChatList_Search_Messages_Channels + case .groups: + filterTitle = presentationData.strings.ChatList_Search_Messages_GroupChats + case .privateChats: + filterTitle = presentationData.strings.ChatList_Search_Messages_PrivateChats + case .globalPosts: + //TODO:localize + filterTitle = "Public Posts" + } + actionTitle = "\(filterTitle) <" } - actionTitle = "\(filterTitle) <" } header = ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings, actionTitle: actionTitle, action: { sourceNode in openMessagesFilter(sourceNode) @@ -1214,12 +1228,20 @@ public enum ChatListSearchEntry: Comparable, Identifiable { filterTitle = presentationData.strings.ChatList_Search_Messages_GroupChats case .privateChats: filterTitle = presentationData.strings.ChatList_Search_Messages_PrivateChats + case .globalPosts: + filterTitle = "Public Posts" } actionTitle = "\(filterTitle) <" - let header = ChatListSearchItemHeader(type: .messages(location: nil), theme: presentationData.theme, strings: presentationData.strings, actionTitle: actionTitle, action: { sourceNode in - openMessagesFilter(sourceNode) - }) + let header: ChatListSearchItemHeader + if key == .globalPosts { + //TODO:localize + header = ChatListSearchItemHeader(type: .text("Public Posts", 0), theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) + } else { + header = ChatListSearchItemHeader(type: .messages(location: nil), theme: presentationData.theme, strings: presentationData.strings, actionTitle: actionTitle, action: { sourceNode in + openMessagesFilter(sourceNode) + }) + } return ChatListItem(presentationData: presentationData, context: context, chatListLocation: location, filterData: nil, index: EngineChatList.Item.Index.chatList(ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: PeerId(0), namespace: Namespaces.Message.Cloud, id: 0), timestamp: 0))), content: .loading, editing: false, hasActiveRevealControls: false, selected: false, header: header, enabledContextActions: nil, hiddenOffset: false, interaction: interaction) case let .emptyMessagesFooter(presentationData, searchScope, searchQuery): var actionTitle: String? @@ -1233,6 +1255,9 @@ public enum ChatListSearchEntry: Comparable, Identifiable { filterTitle = presentationData.strings.ChatList_Search_Messages_GroupChats case .privateChats: filterTitle = presentationData.strings.ChatList_Search_Messages_PrivateChats + case .globalPosts: + //TODO:localize + filterTitle = "Public Posts" } actionTitle = "\(filterTitle) <" @@ -1271,15 +1296,21 @@ public struct ChatListSearchContainerTransition { public let isEmpty: Bool public let isLoading: Bool public let query: String? + public let approvedGlobalPostQuery: String? + public let remainingGlobalSearches: Int + public let globalSearchUnlockTimestamp: Int32? public var animated: Bool - public init(deletions: [ListViewDeleteItem], insertions: [ListViewInsertItem], updates: [ListViewUpdateItem], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, query: String?, animated: Bool) { + public init(deletions: [ListViewDeleteItem], insertions: [ListViewInsertItem], updates: [ListViewUpdateItem], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, query: String?, approvedGlobalPostQuery: String?, remainingGlobalSearches: Int, globalSearchUnlockTimestamp: Int32?, animated: Bool) { self.deletions = deletions self.insertions = insertions self.updates = updates self.displayingResults = displayingResults self.isEmpty = isEmpty self.isLoading = isLoading + self.approvedGlobalPostQuery = approvedGlobalPostQuery + self.remainingGlobalSearches = remainingGlobalSearches + self.globalSearchUnlockTimestamp = globalSearchUnlockTimestamp self.query = query self.animated = animated } @@ -1343,6 +1374,9 @@ public func chatListSearchContainerPreparedTransition( toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, + approvedGlobalPostQuery: String?, + remainingGlobalSearches: Int, + globalSearchUnlockTimestamp: Int32?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, @@ -1358,7 +1392,7 @@ public func chatListSearchContainerPreparedTransition( let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories, openPublicPosts: openPublicPosts, openMessagesFilter: openMessagesFilter, switchMessagesFilter: switchMessagesFilter), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories, openPublicPosts: openPublicPosts, openMessagesFilter: openMessagesFilter, switchMessagesFilter: switchMessagesFilter), directionHint: nil) } - return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, isLoading: isLoading, query: searchQuery, animated: animated) + return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, isLoading: isLoading, query: searchQuery, approvedGlobalPostQuery: approvedGlobalPostQuery, remainingGlobalSearches: remainingGlobalSearches, globalSearchUnlockTimestamp: globalSearchUnlockTimestamp, animated: animated) } private struct ChatListSearchListPaneNodeState: Equatable { @@ -1456,7 +1490,7 @@ private struct DownloadItem: Equatable { private func filteredPeerSearchQueryResults(value: ([FoundPeer], [FoundPeer]), scope: TelegramSearchPeersScope) -> ([FoundPeer], [FoundPeer]) { switch scope { - case .everywhere, .privateChats, .groups: + case .everywhere, .privateChats, .groups, .globalPosts: return value case .channels: return ( @@ -1575,6 +1609,16 @@ final class GlobalPeerSearchContext { } } +struct TelegramGlobalPostSearchState: Equatable { + var remainingSearches: Int + var unlockTimestamp: Int32? + + init(remainingSearches: Int, unlockTimestamp: Int32?) { + self.remainingSearches = remainingSearches + self.unlockTimestamp = unlockTimestamp + } +} + final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private let context: AccountContext private let animationCache: AnimationCache @@ -1620,6 +1664,10 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private var searchQueryValue: String? private var searchOptionsValue: ChatListSearchOptions? + private var approvedGlobalPostQueryValue: String? + private var globalPostSearchStateValue: TelegramGlobalPostSearchState + private var globalPostSearchUnlockTimer: Foundation.Timer? + private var isPremium: Bool = false var isCurrent: Bool = false @@ -1642,6 +1690,10 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private let emptyResultsTextNode: ImmediateTextNode private let emptyResultsAnimationNode: AnimatedStickerNode private var emptyResultsAnimationSize = CGSize() + private var emptyResultsButton: ComponentView? + private var emptyResultsButtonSubtitle: ComponentView? + private var emptyResultsButtonContent: EmptyResultsButton.Content? + private var emptyResultsButtonSubtitleText: String? private var recentEmptyNode: ASDisplayNode? private var emptyRecentTitleNode: ImmediateTextNode? @@ -1668,10 +1720,14 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private var hiddenMediaDisposable: Disposable? private var searchQueryDisposable: Disposable? + private var approvedSearchQueryDisposable: Disposable? private var searchOptionsDisposable: Disposable? private let searchScopePromise = ValuePromise(.everywhere) + private let approvedGlobalPostQuery = ValuePromise(nil, ignoreRepeated: true) + private let globalPostSearchState = Promise() + init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?, parentController: ViewController?, globalPeerSearchContext: GlobalPeerSearchContext?) { self.context = context self.animationCache = animationCache @@ -1685,6 +1741,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let globalPeerSearchContext = globalPeerSearchContext ?? GlobalPeerSearchContext() self.globalPeerSearchContext = globalPeerSearchContext + + self.globalPostSearchStateValue = TelegramGlobalPostSearchState(remainingSearches: 2, unlockTimestamp: nil) var peersFilter = peersFilter if case .forum = location { @@ -1707,6 +1765,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { tagMask = nil case .apps: tagMask = nil + case .globalPosts: + tagMask = nil case .media: tagMask = .photoOrVideo case .downloads: @@ -1778,6 +1838,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { self.emptyResultsTextNode.displaysAsynchronously = false self.emptyResultsTextNode.maximumNumberOfLines = 0 self.emptyResultsTextNode.textAlignment = .center + self.emptyResultsTextNode.lineSpacing = 0.2 self.emptyResultsTextNode.isHidden = true self.emptyResultsAnimationNode = DefaultAnimatedStickerNodeImpl() @@ -1971,8 +2032,62 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let adsHiddenPromise = self.adsHiddenPromise - let foundItems: Signal<([ChatListSearchEntry], Bool)?, NoError> = combineLatest(queue: .mainQueue(), searchQuery, searchOptions, self.searchScopePromise.get(), downloadItems) - |> mapToSignal { [weak self] query, options, searchScope, downloadItems -> Signal<([ChatListSearchEntry], Bool)?, NoError> in + let isPremium = context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId) + ) + |> map { peer -> Bool in + guard case let .user(user) = peer else { + return false + } + return user.isPremium + } + |> distinctUntilChanged + + let globalPostSearchStateType = self.globalPostSearchState.get() + |> map { state -> Bool in + return state.unlockTimestamp != nil + } + |> distinctUntilChanged + + struct FoundRemoteMessages { + var messages: [EngineMessage] + var readCounters: [EnginePeer.Id: EnginePeerReadCounters] + var threadsData: [EngineMessage.Id: MessageHistoryThreadData] + var totalCount: Int32 + + init(messages: [EngineMessage], readCounters: [EnginePeer.Id: EnginePeerReadCounters], threadsData: [EngineMessage.Id: MessageHistoryThreadData], totalCount: Int32) { + self.messages = messages + self.readCounters = readCounters + self.threadsData = threadsData + self.totalCount = totalCount + } + } + + var defaultFoundRemoteMessagesSignal: Signal<([FoundRemoteMessages], Bool), NoError> = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) + if key == .globalPosts { + let searchSignal = context.engine.messages.searchMessages(location: .general(scope: .globalPosts, tags: nil, minDate: nil, maxDate: nil), query: "", state: nil, limit: 50) + |> map { resultData -> ChatListSearchMessagesResult in + let (result, updatedState) = resultData + + return ChatListSearchMessagesResult(query: "", messages: result.messages.map({ EngineMessage($0) }).sorted(by: { $0.index > $1.index }), readStates: result.readStates.mapValues { EnginePeerReadCounters(state: $0, isMuted: false) }, threadInfo: result.threadInfo, hasMore: !result.completed, totalCount: result.totalCount, state: updatedState) + } + + defaultFoundRemoteMessagesSignal = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], true)) + |> then( + searchSignal + |> map { foundMessages -> ([FoundRemoteMessages], Bool) in + var result: [FoundRemoteMessages] = [] + result.append(FoundRemoteMessages(messages: foundMessages.messages, readCounters: foundMessages.readStates, threadsData: foundMessages.threadInfo, totalCount: foundMessages.totalCount)) + return (result, false) + } + ) + } + + let defaultFoundRemoteMessages = Promise<([FoundRemoteMessages], Bool)>() + defaultFoundRemoteMessages.set(defaultFoundRemoteMessagesSignal) + + let foundItems: Signal<([ChatListSearchEntry], Bool)?, NoError> = combineLatest(queue: .mainQueue(), searchQuery, self.approvedGlobalPostQuery.get(), searchOptions, self.searchScopePromise.get(), downloadItems, globalPostSearchStateType) + |> mapToSignal { [weak self] query, approvedGlobalPostQuery, options, searchScope, downloadItems, _ -> Signal<([ChatListSearchEntry], Bool)?, NoError> in if query == nil && options == nil && [.chats, .topics, .channels, .apps].contains(key) { let _ = currentRemotePeers.swap(nil) return .single(nil) @@ -2496,20 +2611,6 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { return ([:], true) } - struct FoundRemoteMessages { - var messages: [EngineMessage] - var readCounters: [EnginePeer.Id: EnginePeerReadCounters] - var threadsData: [EngineMessage.Id: MessageHistoryThreadData] - var totalCount: Int32 - - init(messages: [EngineMessage], readCounters: [EnginePeer.Id: EnginePeerReadCounters], threadsData: [EngineMessage.Id: MessageHistoryThreadData], totalCount: Int32) { - self.messages = messages - self.readCounters = readCounters - self.threadsData = threadsData - self.totalCount = totalCount - } - } - let foundPublicMessages: Signal<([FoundRemoteMessages], Bool), NoError> if key == .chats || key == .publicPosts, let query, query.hasPrefix("#") { let searchSignal = context.engine.messages.searchHashtagPosts(hashtag: finalQuery, state: nil, limit: 10) @@ -2584,13 +2685,29 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) } else if key == .apps { foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) + } else if key == .globalPosts && (finalQuery.isEmpty || approvedGlobalPostQuery != finalQuery) { + if finalQuery.isEmpty { + foundRemoteMessages = defaultFoundRemoteMessages.get() + } else { + foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) + } } else { if !finalQuery.isEmpty { addAppLogEvent(postbox: context.account.postbox, type: "search_global_query") } - let searchSignals: [Signal<(SearchMessagesResult, SearchMessagesState), NoError>] = searchLocations.map { searchLocation in - return context.engine.messages.searchMessages(location: searchLocation, query: finalQuery, state: nil, limit: 50) + let searchSignals: [Signal<(SearchMessagesResult, SearchMessagesState), NoError>] + + if key == .globalPosts { + #if DEBUG && false + searchSignals = [context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: nil, minDate: nil, maxDate: nil), query: finalQuery, state: nil, limit: 50)] + #else + searchSignals = [context.engine.messages.searchMessages(location: .general(scope: .globalPosts, tags: nil, minDate: nil, maxDate: nil), query: finalQuery, state: nil, limit: 50)] + #endif + } else { + searchSignals = searchLocations.map { searchLocation in + return context.engine.messages.searchMessages(location: searchLocation, query: finalQuery, state: nil, limit: 50) + } } let searchSignal = combineLatest(searchSignals) @@ -3486,10 +3603,60 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { self.searchQueryDisposable = (searchQuery |> deliverOnMainQueue).startStrict(next: { [weak self, weak listInteraction, weak chatListInteraction] query in - self?.searchQueryValue = query + guard let self else { + return + } + + if let searchQueryValue = self.searchQueryValue, searchQueryValue == self.approvedGlobalPostQueryValue { + self.approvedGlobalPostQuery.set(nil) + } + self.searchQueryValue = query + listInteraction?.searchTextHighightState = query chatListInteraction?.searchTextHighightState = query }) + self.globalPostSearchState.set(.single(self.globalPostSearchStateValue)) + self.approvedSearchQueryDisposable = (combineLatest(queue: .mainQueue(), self.approvedGlobalPostQuery.get(), self.globalPostSearchState.get(), isPremium) + |> deliverOnMainQueue).startStrict(next: { [weak self] approvedGlobalPostQuery, globalPostSearchState, isPremium in + guard let self else { + return + } + self.approvedGlobalPostQueryValue = approvedGlobalPostQuery + self.globalPostSearchStateValue = globalPostSearchState + self.isPremium = isPremium + + if globalPostSearchState.unlockTimestamp != nil { + if self.globalPostSearchUnlockTimer == nil { + self.globalPostSearchUnlockTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self] _ in + guard let self else { + return + } + + if let unlockTimestamp = self.globalPostSearchStateValue.unlockTimestamp { + var remainingTime: Int32 = unlockTimestamp - Int32(Date().timeIntervalSince1970) + remainingTime = max(0, remainingTime) + if remainingTime == 0 { + if let globalPostSearchUnlockTimer = self.globalPostSearchUnlockTimer { + self.globalPostSearchUnlockTimer = nil + globalPostSearchUnlockTimer.invalidate() + } + + //TODO:localize + var globalPostSearchState = self.globalPostSearchStateValue + globalPostSearchState.unlockTimestamp = nil + globalPostSearchState.remainingSearches = 2 + self.globalPostSearchState.set(.single(globalPostSearchState)) + } + } + }) + } + } else { + if let globalPostSearchUnlockTimer = self.globalPostSearchUnlockTimer { + self.globalPostSearchUnlockTimer = nil + globalPostSearchUnlockTimer.invalidate() + } + } + }) self.searchOptionsDisposable = (searchOptions |> deliverOnMainQueue).startStrict(next: { [weak self] options in @@ -3635,7 +3802,12 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { return state } }, searchPeer: { peer in - }, searchQuery: strongSelf.searchQueryValue, searchOptions: strongSelf.searchOptionsValue, messageContextAction: { message, node, rect, gesture, paneKey, downloadResource in + }, + searchQuery: strongSelf.searchQueryValue, + approvedGlobalPostQuery: strongSelf.approvedGlobalPostQueryValue, + remainingGlobalSearches: strongSelf.globalPostSearchStateValue.remainingSearches, + globalSearchUnlockTimestamp: strongSelf.globalPostSearchStateValue.unlockTimestamp, + searchOptions: strongSelf.searchOptionsValue, messageContextAction: { message, node, rect, gesture, paneKey, downloadResource in interaction.messageContextAction(message, node, rect, gesture, paneKey, downloadResource) }, openClearRecentlyDownloaded: { guard let strongSelf = self else { @@ -4508,7 +4680,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { self.updatedRecentPeersDisposable.dispose() self.deletedMessagesDisposable?.dispose() self.searchQueryDisposable?.dispose() + self.approvedSearchQueryDisposable?.dispose() self.searchOptionsDisposable?.dispose() + self.globalPostSearchUnlockTimer?.invalidate() } override func didLoad() { @@ -4898,15 +5072,150 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let emptyAnimationHeight = self.emptyResultsAnimationSize.height let emptyAnimationSpacing: CGFloat = 8.0 - let emptyTextSpacing: CGFloat = 8.0 - let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing + let emptyTextSpacing: CGFloat + let emptyButtonSpacing: CGFloat = 15.0 + let emptyButtonSubtitleSpacing: CGFloat = 12.0 + + var displayEmptyAnimation = true + if self.key == .globalPosts && self.emptyResultsButtonContent != nil { + displayEmptyAnimation = false + emptyTextSpacing = 18.0 + } else { + emptyTextSpacing = 8.0 + } + + var emptyButtonSize: CGSize? + var emptyButtonSubtitleSize: CGSize? + + if let emptyResultsButtonContent = self.emptyResultsButtonContent { + let emptyResultsButton: ComponentView + if let current = self.emptyResultsButton { + emptyResultsButton = current + } else { + emptyResultsButton = ComponentView() + self.emptyResultsButton = emptyResultsButton + } + + let emptyResultsButtonSizeValue = emptyResultsButton.update( + transition: .immediate, + component: AnyComponent(EmptyResultsButton( + theme: self.presentationData.theme, + strings: self.presentationData.strings, + content: emptyResultsButtonContent, + action: { [weak self] in + guard let self else { + return + } + if !self.isPremium { + guard let navigationController = self.navigationController else { + return + } + let context = self.context + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .ads, forceDark: false, dismissed: nil) + navigationController.pushViewController(controller) + } else { + if let searchQueryValue = self.searchQueryValue, !searchQueryValue.isEmpty, self.approvedGlobalPostQueryValue != searchQueryValue { + self.approvedGlobalPostQuery.set(searchQueryValue) + + var globalPostSearchStateValue = self.globalPostSearchStateValue + if globalPostSearchStateValue.remainingSearches > 0 { + globalPostSearchStateValue.remainingSearches -= 1 + } + if globalPostSearchStateValue.remainingSearches == 0 { + globalPostSearchStateValue.unlockTimestamp = Int32(Date().timeIntervalSince1970) + 30 + } + self.globalPostSearchState.set(.single(globalPostSearchStateValue)) + } + } + } + )), + environment: {}, + containerSize: CGSize(width: min(282.0, size.width - sideInset - 24.0 * 2.0), height: 50.0) + ) + emptyButtonSize = emptyResultsButtonSizeValue + } else { + if let emptyResultsButton = self.emptyResultsButton { + self.emptyResultsButton = nil + emptyResultsButton.view?.removeFromSuperview() + } + } + + if let emptyResultsButtonSubtitleText = self.emptyResultsButtonSubtitleText { + let emptyResultsButtonSubtitle: ComponentView + if let current = self.emptyResultsButtonSubtitle { + emptyResultsButtonSubtitle = current + } else { + emptyResultsButtonSubtitle = ComponentView() + self.emptyResultsButtonSubtitle = emptyResultsButtonSubtitle + } + + let emptyResultsButtonSubtitleSizeValue = emptyResultsButtonSubtitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: emptyResultsButtonSubtitleText, + font: Font.regular(13.0), + textColor: self.presentationData.theme.list.freeTextColor + )), + horizontalAlignment: .center + )), + environment: {}, + containerSize: CGSize(width: size.width - sideInset - 24.0 * 2.0, height: 100.0) + ) + emptyButtonSubtitleSize = emptyResultsButtonSubtitleSizeValue + } else { + if let emptyResultsButtonSubtitle = self.emptyResultsButtonSubtitle { + self.emptyResultsButtonSubtitle = nil + emptyResultsButtonSubtitle.view?.removeFromSuperview() + } + } + + var emptyTotalHeight = emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing + if displayEmptyAnimation { + emptyTotalHeight += emptyAnimationHeight + emptyAnimationSpacing + } + if let emptyButtonSize { + emptyTotalHeight += emptyButtonSize.height + emptyButtonSpacing + } + if let emptyButtonSubtitleSize { + emptyTotalHeight += emptyButtonSubtitleSize.height + emptyButtonSubtitleSpacing + } + let emptyAnimationY = topInset + floorToScreenPixels((visibleHeight - topInset - bottomInset - emptyTotalHeight) / 2.0) + let emptyTitleY: CGFloat + if displayEmptyAnimation { + emptyTitleY = emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + } else { + emptyTitleY = emptyAnimationY + } let textTransition = ContainedViewLayoutTransition.immediate textTransition.updateFrame(node: self.emptyResultsAnimationNode, frame: CGRect(origin: CGPoint(x: sideInset + padding + (size.width - sideInset * 2.0 - padding * 2.0 - self.emptyResultsAnimationSize.width) / 2.0, y: emptyAnimationY), size: self.emptyResultsAnimationSize)) - textTransition.updateFrame(node: self.emptyResultsTitleNode, frame: CGRect(origin: CGPoint(x: sideInset + padding + (size.width - sideInset * 2.0 - padding * 2.0 - emptyTitleSize.width) / 2.0, y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing), size: emptyTitleSize)) - textTransition.updateFrame(node: self.emptyResultsTextNode, frame: CGRect(origin: CGPoint(x: sideInset + padding + (size.width - sideInset * 2.0 - padding * 2.0 - emptyTextSize.width) / 2.0, y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSpacing), size: emptyTextSize)) + textTransition.updateFrame(node: self.emptyResultsTitleNode, frame: CGRect(origin: CGPoint(x: sideInset + padding + (size.width - sideInset * 2.0 - padding * 2.0 - emptyTitleSize.width) / 2.0, y: emptyTitleY), size: emptyTitleSize)) + textTransition.updateFrame(node: self.emptyResultsTextNode, frame: CGRect(origin: CGPoint(x: sideInset + padding + (size.width - sideInset * 2.0 - padding * 2.0 - emptyTextSize.width) / 2.0, y: emptyTitleY + emptyTitleSize.height + emptyTextSpacing), size: emptyTextSize)) self.emptyResultsAnimationNode.updateLayout(size: self.emptyResultsAnimationSize) + + var nextY: CGFloat = emptyTitleY + emptyTitleSize.height + emptyTextSpacing + emptyTextSize.height + if let emptyButtonView = self.emptyResultsButton?.view, let emptyButtonSize { + nextY += emptyButtonSpacing + + if emptyButtonView.superview == nil { + self.view.insertSubview(emptyButtonView, aboveSubview: self.emptyResultsTextNode.view) + } + emptyButtonView.frame = CGRect(origin: CGPoint(x: floor((size.width - emptyButtonSize.width) * 0.5), y: nextY), size: emptyButtonSize) + + nextY += emptyButtonSize.height + } + if let emptyButtonSubtitleView = self.emptyResultsButtonSubtitle?.view, let emptyButtonSubtitleSize { + nextY += emptyButtonSubtitleSpacing + + if emptyButtonSubtitleView.superview == nil { + self.view.insertSubview(emptyButtonSubtitleView, aboveSubview: self.emptyResultsTextNode.view) + } + emptyButtonSubtitleView.frame = CGRect(origin: CGPoint(x: floor((size.width - emptyButtonSubtitleSize.width) * 0.5), y: nextY), size: emptyButtonSubtitleSize) + + nextY += emptyButtonSubtitleSize.height + } } if !hadValidLayout { @@ -5055,33 +5364,101 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if emptyResults { let emptyResultsTitle: String let emptyResultsText: String - if let query = transition.query, !query.isEmpty { - emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResults - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsQueryDescription(query).string - } else { - if let searchOptions = searchOptions, searchOptions.date == nil && searchOptions.peer == nil { - emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResultsFilter - if strongSelf.tagMask == .photoOrVideo { - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerMedia - } else if strongSelf.tagMask == .webPage { - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerLinks - } else if strongSelf.tagMask == .file { - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerFiles - } else if strongSelf.tagMask == .music { - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerMusic - } else if strongSelf.tagMask == .voiceOrInstantVideo { - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerVoice + var emptyResultsButtonContent: EmptyResultsButton.Content? + var emptyResultsButtonSubtitleText: String? + + if strongSelf.key == .globalPosts { + if !strongSelf.isPremium { + emptyResultsButtonContent = .premiumRequired + emptyResultsTitle = "Global Search" + emptyResultsText = "Type a keyword to search all posts\nfrom public channels." + emptyResultsButtonSubtitleText = "Global search is a Premium feature." + } else if let query = transition.query, !query.isEmpty { + if transition.approvedGlobalPostQuery == query { + emptyResultsButtonContent = nil + emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResults + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsQueryDescription(query).string } else { - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsDescription + if transition.remainingGlobalSearches != 0 { + emptyResultsTitle = "Global Search" + emptyResultsText = "Type a keyword to search all posts\nfrom public channels." + if transition.remainingGlobalSearches == 1 { + emptyResultsButtonSubtitleText = "1 free search remaining today." + } else { + emptyResultsButtonSubtitleText = "\(transition.remainingGlobalSearches) free searches remaining today." + } + + emptyResultsButtonContent = .searchQuery(query) + } else { + emptyResultsTitle = "Limit Reached" + emptyResultsText = "You can make up to\n10 search queries per day." + + emptyResultsButtonContent = .paidSearch( + price: 10, + timestamp: transition.globalSearchUnlockTimestamp + ) + } } } else { + if transition.remainingGlobalSearches != 0 { + emptyResultsButtonContent = .searchEmpty + emptyResultsTitle = "Global Search" + emptyResultsText = "Type a keyword to search all posts\nfrom public channels." + + if transition.remainingGlobalSearches == 1 { + emptyResultsButtonSubtitleText = "1 free search remaining today." + } else { + emptyResultsButtonSubtitleText = "\(transition.remainingGlobalSearches) free searches remaining today." + } + } else { + emptyResultsTitle = "Limit Reached" + emptyResultsText = "You can make up to\n10 search queries per day." + + emptyResultsButtonContent = .paidSearch( + price: 10, + timestamp: transition.globalSearchUnlockTimestamp + ) + } + } + } else { + if let query = transition.query, !query.isEmpty { emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResults - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsDescription + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsQueryDescription(query).string + } else { + if let searchOptions = searchOptions, searchOptions.date == nil && searchOptions.peer == nil { + emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResultsFilter + if strongSelf.tagMask == .photoOrVideo { + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerMedia + } else if strongSelf.tagMask == .webPage { + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerLinks + } else if strongSelf.tagMask == .file { + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerFiles + } else if strongSelf.tagMask == .music { + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerMusic + } else if strongSelf.tagMask == .voiceOrInstantVideo { + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerVoice + } else { + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsDescription + } + } else { + emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResults + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsDescription + } } } - strongSelf.emptyResultsTitleNode.attributedText = NSAttributedString(string: emptyResultsTitle, font: Font.semibold(17.0), textColor: strongSelf.presentationData.theme.list.freeTextColor) - strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: emptyResultsText, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor) + if strongSelf.key == .globalPosts { + strongSelf.emptyResultsTitleNode.attributedText = NSAttributedString(string: emptyResultsTitle, font: Font.semibold(17.0), textColor: strongSelf.presentationData.theme.list.itemPrimaryTextColor) + strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: emptyResultsText, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor) + } else { + strongSelf.emptyResultsTitleNode.attributedText = NSAttributedString(string: emptyResultsTitle, font: Font.semibold(17.0), textColor: strongSelf.presentationData.theme.list.freeTextColor) + strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: emptyResultsText, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor) + } + strongSelf.emptyResultsButtonContent = emptyResultsButtonContent + strongSelf.emptyResultsButtonSubtitleText = emptyResultsButtonSubtitleText + } else { + strongSelf.emptyResultsButtonContent = nil + strongSelf.emptyResultsButtonSubtitleText = nil } if let (size, sideInset, bottomInset, visibleHeight, presentationData) = strongSelf.currentParams { @@ -5093,11 +5470,22 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { strongSelf.emptyResultsTitleNode.isHidden = true strongSelf.emptyResultsTextNode.isHidden = true strongSelf.emptyResultsAnimationNode.visibility = false + strongSelf.emptyResultsButton?.view?.isHidden = true + strongSelf.emptyResultsButtonSubtitle?.view?.isHidden = true + } else if strongSelf.key == .globalPosts { + strongSelf.emptyResultsAnimationNode.isHidden = !emptyResults || strongSelf.emptyResultsButtonContent != nil + strongSelf.emptyResultsTitleNode.isHidden = !emptyResults + strongSelf.emptyResultsTextNode.isHidden = !emptyResults + strongSelf.emptyResultsAnimationNode.visibility = emptyResults + strongSelf.emptyResultsButton?.view?.isHidden = !emptyResults + strongSelf.emptyResultsButtonSubtitle?.view?.isHidden = !emptyResults } else { strongSelf.emptyResultsAnimationNode.isHidden = !emptyResults strongSelf.emptyResultsTitleNode.isHidden = !emptyResults strongSelf.emptyResultsTextNode.isHidden = !emptyResults strongSelf.emptyResultsAnimationNode.visibility = emptyResults + strongSelf.emptyResultsButton?.view?.isHidden = emptyResults + strongSelf.emptyResultsButtonSubtitle?.view?.isHidden = emptyResults } var displayPlaceholder = transition.isLoading && (![.chats, .topics, .channels, .apps].contains(strongSelf.key) || (strongSelf.currentEntries?.isEmpty ?? true)) @@ -5393,7 +5781,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { let items = (0 ..< 2).compactMap { _ -> ListViewItem? in switch key { - case .chats, .topics, .channels, .apps, .downloads, .publicPosts: + case .chats, .topics, .channels, .apps, .globalPosts, .downloads, .publicPosts: let message = EngineMessage( stableId: 0, stableVersion: 0, @@ -5738,3 +6126,389 @@ private final class ChatListSearchReferenceContentSource: ContextReferenceConten return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: .bottom) } } + +private final class EmptyResultsButtonSearchContent: Component { + let theme: PresentationTheme + let strings: PresentationStrings + let query: String + + init( + theme: PresentationTheme, + strings: PresentationStrings, + query: String + ) { + self.theme = theme + self.strings = strings + self.query = query + } + + static func ==(lhs: EmptyResultsButtonSearchContent, rhs: EmptyResultsButtonSearchContent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.query != rhs.query { + return false + } + return true + } + + final class View: UIView { + private let icon = ComponentView() + private let text = ComponentView() + private let arrow = ComponentView() + + private var component: EmptyResultsButtonSearchContent? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: EmptyResultsButtonSearchContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let iconSpacing: CGFloat = 0.0 + let arrowSpacing: CGFloat = 0.0 + + self.component = component + + let iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent( + name: "Peer Info/ButtonSearch", + tintColor: component.theme.list.itemCheckColors.foregroundColor, + scaleFactor: 0.8 + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + + let arrowSize = self.arrow.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent( + name: "Item List/DisclosureArrow", + tintColor: component.theme.list.itemCheckColors.foregroundColor, + scaleFactor: 1.0 + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + + //TODO:localize + let string = NSMutableAttributedString() + string.append(NSAttributedString(string: "Search ", font: Font.semibold(17.0), textColor: component.theme.list.itemCheckColors.foregroundColor)) + string.append(NSAttributedString(string: component.query, font: Font.semibold(17.0), textColor: component.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.7))) + + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(string) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - iconSize.width - iconSpacing - arrowSize.width - arrowSpacing, height: 100.0) + ) + let textFrame = CGRect(origin: CGPoint(x: iconSize.width + iconSpacing, y: 0.0), size: textSize) + if let textView = self.text.view { + if textView.superview == nil { + self.addSubview(textView) + } + textView.frame = textFrame + } + + if let iconView = self.icon.view { + if iconView.superview == nil { + self.addSubview(iconView) + } + iconView.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((textSize.height - iconSize.height) * 0.5)), size: iconSize) + } + + if let arrowView = self.arrow.view { + if arrowView.superview == nil { + self.addSubview(arrowView) + } + arrowView.frame = CGRect(origin: CGPoint(x: textFrame.maxX + arrowSpacing, y: floorToScreenPixels((textSize.height - arrowSize.height) * 0.5)), size: arrowSize) + } + + return CGSize(width: iconSize.width + iconSpacing + textSize.width + arrowSpacing + arrowSize.width, height: textSize.height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class EmptyResultsButtonPaidSearchContent: Component { + let theme: PresentationTheme + let strings: PresentationStrings + let price: Int + let unlockTimestamp: Int32? + + init( + theme: PresentationTheme, + strings: PresentationStrings, + price: Int, + unlockTimestamp: Int32? + ) { + self.theme = theme + self.strings = strings + self.price = price + self.unlockTimestamp = unlockTimestamp + } + + static func ==(lhs: EmptyResultsButtonPaidSearchContent, rhs: EmptyResultsButtonPaidSearchContent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.price != rhs.price { + return false + } + if lhs.unlockTimestamp != rhs.unlockTimestamp { + return false + } + return true + } + + final class View: UIView { + private let title = ComponentView() + private let subtitle = ComponentView() + + private var component: EmptyResultsButtonPaidSearchContent? + private var timer: Foundation.Timer? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: EmptyResultsButtonPaidSearchContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let subtitleSpacing: CGFloat = 1.0 + + self.component = component + self.state = state + + //TODO:localize + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "Search for \(component.price) Stars", font: Font.semibold(17.0), textColor: component.theme.list.itemCheckColors.foregroundColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + + var subtitleText = "" + if let unlockTimestamp = component.unlockTimestamp { + var remainingTime: Int32 = unlockTimestamp - Int32(Date().timeIntervalSince1970) + remainingTime = max(0, remainingTime) + subtitleText = "free search unlocks in \(stringForRemainingTime(remainingTime))" + + if self.timer == nil { + self.timer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self] _ in + guard let self else { + return + } + self.state?.updated(transition: .immediate) + }) + } + } else { + if let timer = self.timer { + self.timer = nil + timer.invalidate() + } + } + + let subtitleSize = self.subtitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: subtitleText, font: Font.regular(12.0), textColor: component.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.7))) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + + var contentSize = CGSize() + if subtitleText.isEmpty { + contentSize = titleSize + } else { + contentSize.width = max(titleSize.width, subtitleSize.width) + contentSize.height = titleSize.height + subtitleSpacing + subtitleSize.height + } + + let titleFrame = CGRect(origin: CGPoint(x: floor((contentSize.width - titleSize.width)), y: 0.0), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.frame = titleFrame + } + + let subtitleFrame = CGRect(origin: CGPoint(x: floor((contentSize.width - subtitleSize.width)), y: titleFrame.maxY + subtitleSpacing), size: subtitleSize) + if let subtitleView = self.subtitle.view { + if subtitleView.superview == nil { + self.addSubview(subtitleView) + } + subtitleView.frame = subtitleFrame + } + + return contentSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class EmptyResultsButton: Component { + enum Content: Equatable { + case searchQuery(String) + case searchEmpty + case premiumRequired + case paidSearch(price: Int, timestamp: Int32?) + } + + let theme: PresentationTheme + let strings: PresentationStrings + let content: Content + let action: () -> Void + + init( + theme: PresentationTheme, + strings: PresentationStrings, + content: Content, + action: @escaping () -> Void + ) { + self.theme = theme + self.strings = strings + self.content = content + self.action = action + } + + static func ==(lhs: EmptyResultsButton, rhs: EmptyResultsButton) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.content != rhs.content { + return false + } + return true + } + + final class View: UIView { + private let button = ComponentView() + + private var component: EmptyResultsButton? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: EmptyResultsButton, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let buttonContent: AnyComponentWithIdentity + var isEnabled = true + switch component.content { + case .searchEmpty: + isEnabled = false + buttonContent = AnyComponentWithIdentity(id: "empty", component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Search", + font: Font.medium(17.0), + textColor: component.theme.list.itemCheckColors.foregroundColor + )) + ))) + case .premiumRequired: + buttonContent = AnyComponentWithIdentity(id: "premium", component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Subscribe to Premium", + font: Font.medium(17.0), + textColor: component.theme.list.itemCheckColors.foregroundColor + )) + ))) + case let .searchQuery(query): + buttonContent = AnyComponentWithIdentity(id: "query", component: AnyComponent(EmptyResultsButtonSearchContent(theme: component.theme, strings: component.strings, query: query))) + case let .paidSearch(price, unlockTimestamp): + buttonContent = AnyComponentWithIdentity(id: "paid", component: AnyComponent(EmptyResultsButtonPaidSearchContent(theme: component.theme, strings: component.strings, price: price, unlockTimestamp: unlockTimestamp))) + } + + let size = self.button.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: component.theme.list.itemCheckColors.fillColor, + foreground: component.theme.list.itemCheckColors.foregroundColor, + pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), + cornerRadius: 10.0 + ), + content: buttonContent, + isEnabled: isEnabled, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.action() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 50.0) + ) + if let buttonView = self.button.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(), size: size)) + } + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private func stringForRemainingTime(_ duration: Int32) -> String { + let hours = duration / 3600 + let minutes = duration / 60 % 60 + let seconds = duration % 60 + let durationString: String + if hours > 0 { + durationString = String(format: "%d:%02d", hours, minutes) + } else { + durationString = String(format: "%02d:%02d", minutes, seconds) + } + return durationString +} diff --git a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift index ef5021db86..ac48fb5c95 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift @@ -55,6 +55,7 @@ public enum ChatListSearchPaneKey { case publicPosts case channels case apps + case globalPosts case media case downloads case links @@ -77,6 +78,8 @@ extension ChatListSearchPaneKey { return .channels case .apps: return .apps + case .globalPosts: + return .globalPosts case .media: return .media case .downloads: @@ -107,6 +110,9 @@ func defaultAvailableSearchPanes(isForum: Bool, hasDownloads: Bool, hasPublicPos } result.append(.channels) result.append(.apps) + if !isForum { + result.append(.globalPosts) + } result.append(contentsOf: [.media, .downloads, .links, .files, .music, .voice]) if !hasDownloads { @@ -232,7 +238,10 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD } return } + #if DEBUG + #else self.isAdjacentLoadingEnabled = true + #endif if self.currentPanes[key] != nil { self.currentPaneKey = key diff --git a/submodules/TelegramApi/Sources/Api39.swift b/submodules/TelegramApi/Sources/Api39.swift index 7df8e9d44c..309ca1feb1 100644 --- a/submodules/TelegramApi/Sources/Api39.swift +++ b/submodules/TelegramApi/Sources/Api39.swift @@ -3511,15 +3511,18 @@ public extension Api.functions.channels { } } public extension Api.functions.channels { - static func searchPosts(hashtag: String, offsetRate: Int32, offsetPeer: Api.InputPeer, offsetId: Int32, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func searchPosts(flags: Int32, hashtag: String?, query: String?, offsetRate: Int32, offsetPeer: Api.InputPeer, offsetId: Int32, limit: Int32, allowPaidStars: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-778069893) - serializeString(hashtag, buffer: buffer, boxed: false) + buffer.appendInt32(-221973939) + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(hashtag!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {serializeString(query!, buffer: buffer, boxed: false)} serializeInt32(offsetRate, buffer: buffer, boxed: false) offsetPeer.serialize(buffer, true) serializeInt32(offsetId, buffer: buffer, boxed: false) serializeInt32(limit, buffer: buffer, boxed: false) - return (FunctionDescription(name: "channels.searchPosts", parameters: [("hashtag", String(describing: hashtag)), ("offsetRate", String(describing: offsetRate)), ("offsetPeer", String(describing: offsetPeer)), ("offsetId", String(describing: offsetId)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.Messages? in + if Int(flags) & Int(1 << 2) != 0 {serializeInt64(allowPaidStars!, buffer: buffer, boxed: false)} + return (FunctionDescription(name: "channels.searchPosts", parameters: [("flags", String(describing: flags)), ("hashtag", String(describing: hashtag)), ("query", String(describing: query)), ("offsetRate", String(describing: offsetRate)), ("offsetPeer", String(describing: offsetPeer)), ("offsetId", String(describing: offsetId)), ("limit", String(describing: limit)), ("allowPaidStars", String(describing: allowPaidStars))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.Messages? in let reader = BufferReader(buffer) var result: Api.messages.Messages? if let signature = reader.readInt32() { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift index 23c5789da7..bc9b34b249 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift @@ -86,6 +86,8 @@ public extension TelegramEngine { return false } }.map(EngineRenderedPeer.init) + case .globalPosts: + return [] } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift index c8fd1ef16e..3309c17f61 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift @@ -473,38 +473,64 @@ func _internal_searchMessages(account: Account, location: SearchMessagesLocation folderId = nil } - if case let .general(scope, _, _, _) = location { - switch scope { - case .everywhere: - break - case .channels: - flags |= (1 << 1) - case .groups: - flags |= (1 << 2) - case .privateChats: - flags |= (1 << 3) + if case let .general(scope, _, _, _) = location, case .globalPosts = scope { + remoteSearchResult = account.postbox.transaction { transaction -> (Int32, MessageIndex?, Api.InputPeer) in + var lowerBound: MessageIndex? + if let state = state, let message = state.main.messages.last { + lowerBound = message.index + } + if let lowerBound = lowerBound, let peer = transaction.getPeer(lowerBound.id.peerId), let inputPeer = apiInputPeer(peer) { + return (state?.main.nextRate ?? 0, lowerBound, inputPeer) + } else { + return (0, lowerBound, .inputPeerEmpty) + } } - } - - let filter: Api.MessagesFilter = tags.flatMap { messageFilterForTagMask($0) } ?? .inputMessagesFilterEmpty - remoteSearchResult = account.postbox.transaction { transaction -> (Int32, MessageIndex?, Api.InputPeer) in - var lowerBound: MessageIndex? - if let state = state, let message = state.main.messages.last { - lowerBound = message.index + |> mapToSignal { (nextRate, lowerBound, inputPeer) in + var flags: Int32 = 0 + flags |= 1 << 1 + return account.network.request(Api.functions.channels.searchPosts(flags: flags, hashtag: nil, query: query, offsetRate: nextRate, offsetPeer: inputPeer, offsetId: lowerBound?.id.id ?? 0, limit: limit, allowPaidStars: nil), automaticFloodWait: false) + |> map { result -> (Api.messages.Messages?, Api.messages.Messages?) in + return (result, nil) + } + |> `catch` { _ -> Signal<(Api.messages.Messages?, Api.messages.Messages?), NoError> in + return .single((nil, nil)) + } } - if let lowerBound = lowerBound, let peer = transaction.getPeer(lowerBound.id.peerId), let inputPeer = apiInputPeer(peer) { - return (state?.main.nextRate ?? 0, lowerBound, inputPeer) - } else { - return (0, lowerBound, .inputPeerEmpty) + } else { + if case let .general(scope, _, _, _) = location { + switch scope { + case .everywhere: + break + case .channels: + flags |= (1 << 1) + case .groups: + flags |= (1 << 2) + case .privateChats: + flags |= (1 << 3) + case .globalPosts: + break + } } - } - |> mapToSignal { (nextRate, lowerBound, inputPeer) in - return account.network.request(Api.functions.messages.searchGlobal(flags: flags, folderId: folderId, q: query, filter: filter, minDate: minDate ?? 0, maxDate: maxDate ?? (Int32.max - 1), offsetRate: nextRate, offsetPeer: inputPeer, offsetId: lowerBound?.id.id ?? 0, limit: limit), automaticFloodWait: false) - |> map { result -> (Api.messages.Messages?, Api.messages.Messages?) in - return (result, nil) + let filter: Api.MessagesFilter = tags.flatMap { messageFilterForTagMask($0) } ?? .inputMessagesFilterEmpty + remoteSearchResult = account.postbox.transaction { transaction -> (Int32, MessageIndex?, Api.InputPeer) in + var lowerBound: MessageIndex? + if let state = state, let message = state.main.messages.last { + lowerBound = message.index + } + if let lowerBound = lowerBound, let peer = transaction.getPeer(lowerBound.id.peerId), let inputPeer = apiInputPeer(peer) { + return (state?.main.nextRate ?? 0, lowerBound, inputPeer) + } else { + return (0, lowerBound, .inputPeerEmpty) + } } - |> `catch` { _ -> Signal<(Api.messages.Messages?, Api.messages.Messages?), NoError> in - return .single((nil, nil)) + |> mapToSignal { (nextRate, lowerBound, inputPeer) in + return account.network.request(Api.functions.messages.searchGlobal(flags: flags, folderId: folderId, q: query, filter: filter, minDate: minDate ?? 0, maxDate: maxDate ?? (Int32.max - 1), offsetRate: nextRate, offsetPeer: inputPeer, offsetId: lowerBound?.id.id ?? 0, limit: limit), automaticFloodWait: false) + |> map { result -> (Api.messages.Messages?, Api.messages.Messages?) in + return (result, nil) + } + |> `catch` { _ -> Signal<(Api.messages.Messages?, Api.messages.Messages?), NoError> in + return .single((nil, nil)) + } } } case let .sentMedia(tags): @@ -594,7 +620,7 @@ func _internal_searchHashtagPosts(account: Account, hashtag: String, state: Sear } } |> mapToSignal { (nextRate, lowerBound, inputPeer) in - return account.network.request(Api.functions.channels.searchPosts(hashtag: hashtag, offsetRate: nextRate, offsetPeer: inputPeer, offsetId: lowerBound?.id.id ?? 0, limit: limit), automaticFloodWait: false) + return account.network.request(Api.functions.channels.searchPosts(flags: 1 << 0, hashtag: hashtag, query: nil, offsetRate: nextRate, offsetPeer: inputPeer, offsetId: lowerBound?.id.id ?? 0, limit: limit, allowPaidStars: nil), automaticFloodWait: false) |> map { result -> (Api.messages.Messages?, Api.messages.Messages?) in return (result, nil) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift index 9b3d384efc..d983b9b1f7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift @@ -23,6 +23,7 @@ public enum TelegramSearchPeersScope { case channels case groups case privateChats + case globalPosts } public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, network: Network, query: String, scope: TelegramSearchPeersScope) -> Signal<([FoundPeer], [FoundPeer]), NoError> { @@ -138,6 +139,9 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo return false } } + case .globalPosts: + renderedMyPeers = [] + renderedPeers = [] } return (renderedMyPeers, renderedPeers) diff --git a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift index 464a016b74..8d08f8da01 100644 --- a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift +++ b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift @@ -510,6 +510,8 @@ public final class ButtonComponent: Component { animateIn = true contentView.isUserInteractionEnabled = false self.addSubview(contentView) + + contentItem.view.parentState = state } let contentFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) * 0.5), y: floorToScreenPixels((availableSize.height - contentSize.height) * 0.5)), size: contentSize) From b8ac955792d45684f99701c5aae27118123ecde7 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 25 Jul 2025 17:40:09 +0200 Subject: [PATCH 11/11] Update --- MODULE.bazel.lock | 20 +-- submodules/ChatListUI/BUILD | 1 + .../Sources/ChatListSearchListPaneNode.swift | 164 +++++++++++++----- .../Messages/SearchMessages.swift | 7 +- .../TelegramEngine/Peers/SearchPeers.swift | 4 +- .../Sources/PeerInfoRatingComponent.swift | 110 ++++++------ .../Sources/PeerInfoHeaderNode.swift | 19 +- .../Chat/ChatControllerPaidMessage.swift | 2 +- ...rollerOpenMessageReactionContextMenu.swift | 4 +- .../Sources/UndoOverlayController.swift | 2 +- .../Sources/UndoOverlayControllerNode.swift | 18 +- 11 files changed, 230 insertions(+), 121 deletions(-) diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index ed86535e8b..12914dd1dc 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -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.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.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.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.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.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/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198" }, "selectedYankedVersions": {}, "moduleExtensions": { "@@apple_support+//crosstool:setup.bzl%apple_cc_configure_extension": { "general": { - "bzlTransitiveDigest": "RjubjYIojbv0PxTpnoknalV9QzT9asbV7elDuN7m2A4=", + "bzlTransitiveDigest": "IK7QnlhcNBu2jc4wZoGZeDTu3keF2LldFiFUINRcKvo=", "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": "hUTp2w+RUVdL7ma5esCXZJAFnX7vLbVfLd7FwnQI6bU=", + "bzlTransitiveDigest": "sFhcgPbDQehmbD1EOXzX4H1q/CD5df8zwG4kp4jbvr8=", "usagesDigest": "QI2z8ZUR+mqtbwsf2fLqYdJAkPOHdOV+tF2yVAUgRzw=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -290,7 +290,7 @@ }, "@@rules_xcodeproj+//xcodeproj:extensions.bzl%internal": { "general": { - "bzlTransitiveDigest": "6MYik+6MZUO7rOzaI0dUJYVD8dJrR1Q2rT+5vo1j73U=", + "bzlTransitiveDigest": "+kmqZtEKFY8zgqpV6mrwdQkTJqGUZhL8b3ZMsxrqSyc=", "usagesDigest": "fvsnMonVwKDYnBfww4bXuYie3WU0d9VSqT2gePSdQco=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -312,7 +312,7 @@ }, "@@rules_xcodeproj+//xcodeproj:extensions.bzl%non_module_deps": { "general": { - "bzlTransitiveDigest": "6MYik+6MZUO7rOzaI0dUJYVD8dJrR1Q2rT+5vo1j73U=", + "bzlTransitiveDigest": "+kmqZtEKFY8zgqpV6mrwdQkTJqGUZhL8b3ZMsxrqSyc=", "usagesDigest": "jzxYhnOC9BE0dJ0biFLfxWXi/+R19uAAZkJ0p9CY0JI=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index 9d17f95213..29031671ee 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -116,6 +116,7 @@ swift_library( "//submodules/TelegramUI/Components/Ads/AdsInfoScreen", "//submodules/TelegramUI/Components/Ads/AdsReportScreen", "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/AnimatedTextComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 24e109925c..17b1ec5b10 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -37,6 +37,7 @@ import ComponentFlow import MultilineTextComponent import ButtonComponent import BundleIconComponent +import AnimatedTextComponent private enum ChatListRecentEntryStableId: Hashable { case topPeers @@ -1296,19 +1297,19 @@ public struct ChatListSearchContainerTransition { public let isEmpty: Bool public let isLoading: Bool public let query: String? - public let approvedGlobalPostQuery: String? + public let approvedGlobalPostQueryState: ApprovedGlobalPostQueryState? public let remainingGlobalSearches: Int public let globalSearchUnlockTimestamp: Int32? public var animated: Bool - public init(deletions: [ListViewDeleteItem], insertions: [ListViewInsertItem], updates: [ListViewUpdateItem], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, query: String?, approvedGlobalPostQuery: String?, remainingGlobalSearches: Int, globalSearchUnlockTimestamp: Int32?, animated: Bool) { + public init(deletions: [ListViewDeleteItem], insertions: [ListViewInsertItem], updates: [ListViewUpdateItem], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, query: String?, approvedGlobalPostQueryState: ApprovedGlobalPostQueryState?, remainingGlobalSearches: Int, globalSearchUnlockTimestamp: Int32?, animated: Bool) { self.deletions = deletions self.insertions = insertions self.updates = updates self.displayingResults = displayingResults self.isEmpty = isEmpty self.isLoading = isLoading - self.approvedGlobalPostQuery = approvedGlobalPostQuery + self.approvedGlobalPostQueryState = approvedGlobalPostQueryState self.remainingGlobalSearches = remainingGlobalSearches self.globalSearchUnlockTimestamp = globalSearchUnlockTimestamp self.query = query @@ -1374,7 +1375,7 @@ public func chatListSearchContainerPreparedTransition( toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, - approvedGlobalPostQuery: String?, + approvedGlobalPostQueryState: ApprovedGlobalPostQueryState?, remainingGlobalSearches: Int, globalSearchUnlockTimestamp: Int32?, searchOptions: ChatListSearchOptions?, @@ -1392,7 +1393,7 @@ public func chatListSearchContainerPreparedTransition( let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories, openPublicPosts: openPublicPosts, openMessagesFilter: openMessagesFilter, switchMessagesFilter: switchMessagesFilter), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories, openPublicPosts: openPublicPosts, openMessagesFilter: openMessagesFilter, switchMessagesFilter: switchMessagesFilter), directionHint: nil) } - return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, isLoading: isLoading, query: searchQuery, approvedGlobalPostQuery: approvedGlobalPostQuery, remainingGlobalSearches: remainingGlobalSearches, globalSearchUnlockTimestamp: globalSearchUnlockTimestamp, animated: animated) + return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, isLoading: isLoading, query: searchQuery, approvedGlobalPostQueryState: approvedGlobalPostQueryState, remainingGlobalSearches: remainingGlobalSearches, globalSearchUnlockTimestamp: globalSearchUnlockTimestamp, animated: animated) } private struct ChatListSearchListPaneNodeState: Equatable { @@ -1609,6 +1610,16 @@ final class GlobalPeerSearchContext { } } +public struct ApprovedGlobalPostQueryState: Equatable { + public var query: String + public var price: Int? + + public init(query: String, price: Int?) { + self.query = query + self.price = price + } +} + struct TelegramGlobalPostSearchState: Equatable { var remainingSearches: Int var unlockTimestamp: Int32? @@ -1664,7 +1675,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private var searchQueryValue: String? private var searchOptionsValue: ChatListSearchOptions? - private var approvedGlobalPostQueryValue: String? + private var approvedGlobalPostQueryStateValue: ApprovedGlobalPostQueryState? private var globalPostSearchStateValue: TelegramGlobalPostSearchState private var globalPostSearchUnlockTimer: Foundation.Timer? private var isPremium: Bool = false @@ -1725,7 +1736,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private let searchScopePromise = ValuePromise(.everywhere) - private let approvedGlobalPostQuery = ValuePromise(nil, ignoreRepeated: true) + private let approvedGlobalPostQueryState = ValuePromise(nil, ignoreRepeated: true) private let globalPostSearchState = Promise() init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?, parentController: ViewController?, globalPeerSearchContext: GlobalPeerSearchContext?) { @@ -2065,7 +2076,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { var defaultFoundRemoteMessagesSignal: Signal<([FoundRemoteMessages], Bool), NoError> = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) if key == .globalPosts { - let searchSignal = context.engine.messages.searchMessages(location: .general(scope: .globalPosts, tags: nil, minDate: nil, maxDate: nil), query: "", state: nil, limit: 50) + let searchSignal = context.engine.messages.searchMessages(location: .general(scope: .globalPosts(allowPaidStars: nil), tags: nil, minDate: nil, maxDate: nil), query: "", state: nil, limit: 50) |> map { resultData -> ChatListSearchMessagesResult in let (result, updatedState) = resultData @@ -2086,8 +2097,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let defaultFoundRemoteMessages = Promise<([FoundRemoteMessages], Bool)>() defaultFoundRemoteMessages.set(defaultFoundRemoteMessagesSignal) - let foundItems: Signal<([ChatListSearchEntry], Bool)?, NoError> = combineLatest(queue: .mainQueue(), searchQuery, self.approvedGlobalPostQuery.get(), searchOptions, self.searchScopePromise.get(), downloadItems, globalPostSearchStateType) - |> mapToSignal { [weak self] query, approvedGlobalPostQuery, options, searchScope, downloadItems, _ -> Signal<([ChatListSearchEntry], Bool)?, NoError> in + let foundItems: Signal<([ChatListSearchEntry], Bool, String?)?, NoError> = combineLatest(queue: .mainQueue(), searchQuery, self.approvedGlobalPostQueryState.get(), searchOptions, self.searchScopePromise.get(), downloadItems, globalPostSearchStateType) + |> debounceOnMainThread + |> mapToSignal { [weak self] query, approvedGlobalPostQueryState, options, searchScope, downloadItems, _ -> Signal<([ChatListSearchEntry], Bool, String?)?, NoError> in if query == nil && options == nil && [.chats, .topics, .channels, .apps].contains(key) { let _ = currentRemotePeers.swap(nil) return .single(nil) @@ -2114,7 +2126,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } return combineLatest(queue: .mainQueue(), presentationDataPromise.get(), selectionPromise.get()) - |> map { presentationData, selectionState -> ([ChatListSearchEntry], Bool)? in + |> map { presentationData, selectionState -> ([ChatListSearchEntry], Bool, String?)? in var entries: [ChatListSearchEntry] = [] var existingMessageIds = Set() @@ -2182,7 +2194,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { entries.append(.message(message, peer, nil, nil, presentationData, 1, selectionState?.contains(message.id), false, .downloaded(timestamp: item.timestamp, index: message.index), (item.resourceId, item.size, false), .recentlyDownloaded, false, nil, false, .everywhere)) } - return (entries.sorted(), false) + return (entries.sorted(), false, query) } } @@ -2685,7 +2697,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) } else if key == .apps { foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) - } else if key == .globalPosts && (finalQuery.isEmpty || approvedGlobalPostQuery != finalQuery) { + } else if key == .globalPosts && (finalQuery.isEmpty || approvedGlobalPostQueryState?.query != finalQuery) { if finalQuery.isEmpty { foundRemoteMessages = defaultFoundRemoteMessages.get() } else { @@ -2699,11 +2711,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let searchSignals: [Signal<(SearchMessagesResult, SearchMessagesState), NoError>] if key == .globalPosts { - #if DEBUG && false - searchSignals = [context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: nil, minDate: nil, maxDate: nil), query: finalQuery, state: nil, limit: 50)] - #else - searchSignals = [context.engine.messages.searchMessages(location: .general(scope: .globalPosts, tags: nil, minDate: nil, maxDate: nil), query: finalQuery, state: nil, limit: 50)] - #endif + searchSignals = [context.engine.messages.searchMessages(location: .general(scope: .globalPosts(allowPaidStars: approvedGlobalPostQueryState?.price), tags: nil, minDate: nil, maxDate: nil), query: finalQuery, state: nil, limit: 50)] } else { searchSignals = searchLocations.map { searchLocation in return context.engine.messages.searchMessages(location: searchLocation, query: finalQuery, state: nil, limit: 50) @@ -2863,7 +2871,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { foundThreads, adsHiddenPromise.get() ) - |> map { accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, foundPublicMessages, presentationData, searchState, selectionState, resolvedMessage, recentPeers, allAndFoundThreads, adsHidden -> ([ChatListSearchEntry], Bool)? in + |> map { accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, foundPublicMessages, presentationData, searchState, selectionState, resolvedMessage, recentPeers, allAndFoundThreads, adsHidden -> ([ChatListSearchEntry], Bool, String?)? in let isSearching = foundRemotePeers.3 || foundRemoteMessages.1 || foundPublicMessages.1 var entries: [ChatListSearchEntry] = [] var index = 0 @@ -3309,7 +3317,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { entries.append(.addContact(finalQuery, presentationData.theme, presentationData.strings)) } - return (entries, isSearching) + return (entries, isSearching, query) } } @@ -3597,6 +3605,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { listInteraction.preferredStoryHighQuality = context.sharedContext.currentAutomaticMediaDownloadSettings.highQualityStories let previousSearchItems = Atomic<[ChatListSearchEntry]?>(value: nil) + let previousSearchQuery = Atomic(value: nil) let previousSelectedMessages = Atomic?>(value: nil) let previousExpandGlobalSearch = Atomic(value: false) let previousAdsHidden = Atomic(value: false) @@ -3607,8 +3616,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { return } - if let searchQueryValue = self.searchQueryValue, searchQueryValue == self.approvedGlobalPostQueryValue { - self.approvedGlobalPostQuery.set(nil) + if let searchQueryValue = self.searchQueryValue, searchQueryValue == self.approvedGlobalPostQueryStateValue?.query { + self.approvedGlobalPostQueryState.set(nil) } self.searchQueryValue = query @@ -3616,12 +3625,12 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { chatListInteraction?.searchTextHighightState = query }) self.globalPostSearchState.set(.single(self.globalPostSearchStateValue)) - self.approvedSearchQueryDisposable = (combineLatest(queue: .mainQueue(), self.approvedGlobalPostQuery.get(), self.globalPostSearchState.get(), isPremium) - |> deliverOnMainQueue).startStrict(next: { [weak self] approvedGlobalPostQuery, globalPostSearchState, isPremium in + self.approvedSearchQueryDisposable = (combineLatest(queue: .mainQueue(), self.approvedGlobalPostQueryState.get(), self.globalPostSearchState.get(), isPremium) + |> deliverOnMainQueue).startStrict(next: { [weak self] approvedGlobalPostQueryState, globalPostSearchState, isPremium in guard let self else { return } - self.approvedGlobalPostQueryValue = approvedGlobalPostQuery + self.approvedGlobalPostQueryStateValue = approvedGlobalPostQueryState self.globalPostSearchStateValue = globalPostSearchState self.isPremium = isPremium @@ -3664,8 +3673,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }) - self.searchDisposable.set((foundItems |> mapToSignal { items -> Signal<([ChatListSearchEntry], Bool)?, NoError> in - guard let (items, isSearching) = items else { + self.searchDisposable.set((foundItems |> mapToSignal { items -> Signal<([ChatListSearchEntry], Bool, String?)?, NoError> in + guard let (items, isSearching, query) = items else { return .single(nil) } var storyStatsIds: [EnginePeer.Id] = [] @@ -3708,7 +3717,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { requiresPremiumForMessagingPeerIds.map(TelegramEngine.EngineData.Item.Peer.IsPremiumRequiredForMessaging.init(id:)) ) ) - |> map { stats, requiresPremiumForMessaging -> ([ChatListSearchEntry], Bool)? in + |> map { stats, requiresPremiumForMessaging -> ([ChatListSearchEntry], Bool, String?)? in var requiresPremiumForMessaging = requiresPremiumForMessaging if !peersFilter.contains(.onlyWriteable) { requiresPremiumForMessaging = [:] @@ -3731,7 +3740,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { break } } - return (mappedItems, isSearching) + return (mappedItems, isSearching, query) } } |> deliverOnMainQueue).startStrict(next: { [weak self] foundItems in @@ -3743,6 +3752,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { var entriesAndFlags = foundItems?.0 let isSearching = foundItems?.1 ?? false + let currentQuery = foundItems?.2 strongSelf._isSearching.set(isSearching) if strongSelf.tagMask == .photoOrVideo { @@ -3773,6 +3783,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } let previousEntries = previousSearchItems.swap(entriesAndFlags) + let previousQuery = previousSearchQuery.swap(currentQuery) let newEntries = entriesAndFlags ?? [] let selectionChanged = (previousSelectedMessageIds == nil) != (strongSelf.selectedMessages == nil) @@ -3780,7 +3791,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let adsHiddenChanged = previousAdsHidden != strongSelf.adsHidden let animated = selectionChanged || expandGlobalSearchChanged || adsHiddenChanged - let firstTime = previousEntries == nil + let firstTime = previousEntries == nil || previousQuery != currentQuery var transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: newEntries, displayingResults: entriesAndFlags != nil, isEmpty: !isSearching && (entriesAndFlags?.isEmpty ?? false), isLoading: isSearching, animated: animated, context: context, presentationData: strongSelf.presentationData, enableHeaders: true, filter: peersFilter, requestPeerType: requestPeerType, location: location, key: strongSelf.key, tagMask: tagMask, interaction: chatListInteraction, listInteraction: listInteraction, peerContextAction: { message, node, rect, gesture, location in interaction.peerContextAction?(message, node, rect, gesture, location) }, toggleExpandLocalResults: { @@ -3804,7 +3815,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }, searchPeer: { peer in }, searchQuery: strongSelf.searchQueryValue, - approvedGlobalPostQuery: strongSelf.approvedGlobalPostQueryValue, + approvedGlobalPostQueryState: strongSelf.approvedGlobalPostQueryStateValue, remainingGlobalSearches: strongSelf.globalPostSearchStateValue.remainingSearches, globalSearchUnlockTimestamp: strongSelf.globalPostSearchStateValue.unlockTimestamp, searchOptions: strongSelf.searchOptionsValue, messageContextAction: { message, node, rect, gesture, paneKey, downloadResource in @@ -5114,17 +5125,47 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let controller = context.sharedContext.makePremiumIntroController(context: context, source: .ads, forceDark: false, dismissed: nil) navigationController.pushViewController(controller) } else { - if let searchQueryValue = self.searchQueryValue, !searchQueryValue.isEmpty, self.approvedGlobalPostQueryValue != searchQueryValue { - self.approvedGlobalPostQuery.set(searchQueryValue) + if let searchQueryValue = self.searchQueryValue, !searchQueryValue.isEmpty, self.approvedGlobalPostQueryStateValue?.query != searchQueryValue { + var price: Int? var globalPostSearchStateValue = self.globalPostSearchStateValue + if globalPostSearchStateValue.remainingSearches == 0 { + //TODO:localize + price = 10 + } + + self.approvedGlobalPostQueryState.set(ApprovedGlobalPostQueryState( + query: searchQueryValue, + price: price + )) + + if globalPostSearchStateValue.remainingSearches > 0 { globalPostSearchStateValue.remainingSearches -= 1 - } - if globalPostSearchStateValue.remainingSearches == 0 { - globalPostSearchStateValue.unlockTimestamp = Int32(Date().timeIntervalSince1970) + 30 + if globalPostSearchStateValue.remainingSearches == 0 { + globalPostSearchStateValue.unlockTimestamp = Int32(Date().timeIntervalSince1970) + 30 + } } self.globalPostSearchState.set(.single(globalPostSearchStateValue)) + + if let price { + //TODO:localize + if let controller = self.navigationController?.topViewController as? ViewController { + controller.present(UndoOverlayController( + presentationData: presentationData, + content: .starsSent(context: self.context, title: "", text: [AnimatedTextComponent.Item( + id: AnyHashable(0), + isUnbreakable: true, + content: .text("\(price) Stars spent on extra search."), + )], hasUndo: false), + elevatedLayout: false, + animateInAsReplacement: false, + action: { action in + return true + } + ), in: .current) + } + } } } } @@ -5336,7 +5377,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { self.enqueuedTransitions.remove(at: 0) var options = ListViewDeleteAndInsertOptions() - if isFirstTime && [.chats, .topics, .channels, .apps].contains(self.key) { + if isFirstTime && [.chats, .topics, .channels, .apps, .globalPosts].contains(self.key) { options.insert(.PreferSynchronousDrawing) options.insert(.PreferSynchronousResourceLoading) } else if transition.animated { @@ -5374,7 +5415,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { emptyResultsText = "Type a keyword to search all posts\nfrom public channels." emptyResultsButtonSubtitleText = "Global search is a Premium feature." } else if let query = transition.query, !query.isEmpty { - if transition.approvedGlobalPostQuery == query { + if transition.approvedGlobalPostQueryState?.query == query { emptyResultsButtonContent = nil emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResults emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsQueryDescription(query).string @@ -6288,6 +6329,8 @@ private final class EmptyResultsButtonPaidSearchContent: Component { private var timer: Foundation.Timer? private weak var state: EmptyComponentState? + private var cachedStarImage: UIImage? + override init(frame: CGRect) { super.init(frame: frame) } @@ -6299,14 +6342,25 @@ private final class EmptyResultsButtonPaidSearchContent: Component { func update(component: EmptyResultsButtonPaidSearchContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let subtitleSpacing: CGFloat = 1.0 + if self.cachedStarImage == nil || self.component?.theme !== component.theme { + self.cachedStarImage = generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: component.theme.list.itemCheckColors.foregroundColor) + } + self.component = component self.state = state + let attributedString = NSMutableAttributedString(attributedString: NSAttributedString(string: "Search for * \(component.price)", font: Font.semibold(17.0), textColor: component.theme.list.itemCheckColors.foregroundColor)) + if let range = attributedString.string.range(of: "*"), let starImage = self.cachedStarImage { + attributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: attributedString.string)) + attributedString.addAttribute(.foregroundColor, value: component.theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: attributedString.string)) + attributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedString.string)) + } + //TODO:localize let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: "Search for \(component.price) Stars", font: Font.semibold(17.0), textColor: component.theme.list.itemCheckColors.foregroundColor)) + text: .plain(attributedString) )), environment: {}, containerSize: CGSize(width: availableSize.width, height: 100.0) @@ -6349,8 +6403,9 @@ private final class EmptyResultsButtonPaidSearchContent: Component { contentSize.width = max(titleSize.width, subtitleSize.width) contentSize.height = titleSize.height + subtitleSpacing + subtitleSize.height } + contentSize.width = max(contentSize.width, availableSize.width) - let titleFrame = CGRect(origin: CGPoint(x: floor((contentSize.width - titleSize.width)), y: 0.0), size: titleSize) + let titleFrame = CGRect(origin: CGPoint(x: floor((contentSize.width - titleSize.width) * 0.5), y: 0.0), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { self.addSubview(titleView) @@ -6358,7 +6413,7 @@ private final class EmptyResultsButtonPaidSearchContent: Component { titleView.frame = titleFrame } - let subtitleFrame = CGRect(origin: CGPoint(x: floor((contentSize.width - subtitleSize.width)), y: titleFrame.maxY + subtitleSpacing), size: subtitleSize) + let subtitleFrame = CGRect(origin: CGPoint(x: floor((contentSize.width - subtitleSize.width) * 0.5), y: titleFrame.maxY + subtitleSpacing), size: subtitleSize) if let subtitleView = self.subtitle.view { if subtitleView.superview == nil { self.addSubview(subtitleView) @@ -6512,3 +6567,30 @@ private func stringForRemainingTime(_ duration: Int32) -> String { } return durationString } + +func debounceOnMainThread(_ signal: Signal) -> Signal { + return Signal { subscriber in + let value = Atomic(value: nil) + + let flushValue: () -> Void = { + let v = value.swap(nil) + if let v { + subscriber.putNext(v) + } + } + + return signal.start(next: { v in + let previous = value.swap(v) + if previous == nil { + DispatchQueue.main.async { + flushValue() + } + } + }, error: { e in + subscriber.putError(e) + }, completed: { + flushValue() + subscriber.putCompletion() + }) + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift index 3309c17f61..8d58e29bda 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift @@ -473,7 +473,7 @@ func _internal_searchMessages(account: Account, location: SearchMessagesLocation folderId = nil } - if case let .general(scope, _, _, _) = location, case .globalPosts = scope { + if case let .general(scope, _, _, _) = location, case let .globalPosts(allowPaidStars) = scope { remoteSearchResult = account.postbox.transaction { transaction -> (Int32, MessageIndex?, Api.InputPeer) in var lowerBound: MessageIndex? if let state = state, let message = state.main.messages.last { @@ -488,7 +488,10 @@ func _internal_searchMessages(account: Account, location: SearchMessagesLocation |> mapToSignal { (nextRate, lowerBound, inputPeer) in var flags: Int32 = 0 flags |= 1 << 1 - return account.network.request(Api.functions.channels.searchPosts(flags: flags, hashtag: nil, query: query, offsetRate: nextRate, offsetPeer: inputPeer, offsetId: lowerBound?.id.id ?? 0, limit: limit, allowPaidStars: nil), automaticFloodWait: false) + if allowPaidStars != nil { + flags |= 1 << 2 + } + return account.network.request(Api.functions.channels.searchPosts(flags: flags, hashtag: nil, query: query, offsetRate: nextRate, offsetPeer: inputPeer, offsetId: lowerBound?.id.id ?? 0, limit: limit, allowPaidStars: allowPaidStars.flatMap(Int64.init)), automaticFloodWait: false) |> map { result -> (Api.messages.Messages?, Api.messages.Messages?) in return (result, nil) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift index d983b9b1f7..1e924f8faf 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift @@ -18,12 +18,12 @@ public struct FoundPeer: Equatable { } } -public enum TelegramSearchPeersScope { +public enum TelegramSearchPeersScope: Equatable { case everywhere case channels case groups case privateChats - case globalPosts + case globalPosts(allowPaidStars: Int?) } public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, network: Network, query: String, scope: TelegramSearchPeersScope) -> Signal<([FoundPeer], [FoundPeer]), NoError> { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift index 40d8136193..d0e013b307 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift @@ -152,8 +152,17 @@ public final class PeerInfoRatingComponent: Component { let baseHeight: CGFloat = 20.0 let innerInset: CGFloat = 2.0 + let compactLabelSize = self.compactLabel.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.compactLabel, font: Font.medium(11.0), textColor: .black)) + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let expandedSize = CGSize(width: 174.0, height: baseHeight) - let collapsedSize = CGSize(width: baseHeight, height: baseHeight) + let collapsedSize = CGSize(width: max(baseHeight, compactLabelSize.width + 6.0 * 2.0), height: baseHeight) if self.backgroundView.image == nil { self.backgroundView.image = generateStretchableFilledCircleImage(diameter: baseHeight, color: .white)?.withRenderingMode(.alwaysTemplate) @@ -189,19 +198,11 @@ public final class PeerInfoRatingComponent: Component { self.foregroundClippedView.backgroundColor = component.foregroundColor transition.setFrame(view: self.foregroundClippedShapeView, frame: foregroundFrame) - let compactLabelSize = self.compactLabel.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: component.compactLabel, font: Font.medium(11.0), textColor: .black)) - )), - environment: {}, - containerSize: CGSize(width: 100.0, height: 100.0) - ) if let compactLabelView = self.compactLabel.view { if compactLabelView.superview == nil { self.foregroundMaskView.addSubview(compactLabelView) } - compactLabelView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((baseHeight - innerInset * 2.0 - compactLabelSize.width) * 0.5), y: floorToScreenPixels((baseHeight - innerInset * 2.0 - compactLabelSize.height) * 0.5) + UIScreenPixel), size: compactLabelSize) + compactLabelView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((collapsedSize.width - innerInset * 2.0 - compactLabelSize.width) * 0.5), y: floorToScreenPixels((baseHeight - innerInset * 2.0 - compactLabelSize.height) * 0.5) + UIScreenPixel), size: compactLabelSize) alphaTransition.setAlpha(view: compactLabelView, alpha: component.isExpanded ? 0.0 : 1.0) } @@ -284,50 +285,57 @@ public final class PeerInfoRatingComponent: Component { }) } - let tooltipController: TooltipScreen - if let current = self.tooltipController { - tooltipController = current + if !component.tooltipLabel.isEmpty { + let tooltipController: TooltipScreen + if let current = self.tooltipController { + tooltipController = current + } else { + tooltipController = TooltipScreen( + context: component.context, + account: component.context.account, + sharedContext: component.context.sharedContext, + text: .attributedString(text: NSAttributedString(string: component.tooltipLabel, font: Font.semibold(11.0), textColor: .white)), + style: .customBlur(component.tooltipBackgroundColor, -4.0), + arrowStyle: .small, + location: .point(CGRect(origin: CGPoint(x: 100.0, y: 100.0), size: CGSize()), .bottom), + displayDuration: .infinite, + isShimmering: true, + cornerRadius: 10.0, + shouldDismissOnTouch: { _, _ in + return .ignore + } + ) + self.tooltipController = tooltipController + + tooltipController.containerLayoutUpdated(ContainerViewLayout( + size: CGSize(width: 200.0, height: 200.0), + metrics: LayoutMetrics(), + deviceMetrics: DeviceMetrics.iPhoneXSMax, + intrinsicInsets: UIEdgeInsets(), + safeInsets: UIEdgeInsets(), + additionalInsets: UIEdgeInsets(), + statusBarHeight: nil, + inputHeight: nil, + inputHeightIsInteractivellyChanging: false, + inVoiceOver: false + ), transition: .immediate) + + self.layer.addSublayer(tooltipController.view.layer) + tooltipController.viewWillAppear(false) + tooltipController.viewDidAppear(false) + tooltipController.setIgnoreAppearanceMethodInvocations(true) + tooltipController.view.isUserInteractionEnabled = false + } + + transition.setFrame(view: tooltipController.view, frame: CGRect(origin: CGPoint(), size: CGSize(width: 200.0, height: 200.0)).offsetBy(dx: -200.0 * 0.5 + foregroundFrame.width + 2.0, dy: -200.0 * 0.5)) + alphaTransition.setAlpha(view: tooltipController.view, alpha: component.isExpanded ? 1.0 : 0.0) } else { - tooltipController = TooltipScreen( - context: component.context, - account: component.context.account, - sharedContext: component.context.sharedContext, - text: .attributedString(text: NSAttributedString(string: component.tooltipLabel, font: Font.semibold(11.0), textColor: .white)), - style: .customBlur(component.tooltipBackgroundColor, -4.0), - arrowStyle: .small, - location: .point(CGRect(origin: CGPoint(x: 100.0, y: 100.0), size: CGSize()), .bottom), - displayDuration: .infinite, - isShimmering: true, - cornerRadius: 10.0, - shouldDismissOnTouch: { _, _ in - return .ignore - } - ) - self.tooltipController = tooltipController - - tooltipController.containerLayoutUpdated(ContainerViewLayout( - size: CGSize(width: 200.0, height: 200.0), - metrics: LayoutMetrics(), - deviceMetrics: DeviceMetrics.iPhoneXSMax, - intrinsicInsets: UIEdgeInsets(), - safeInsets: UIEdgeInsets(), - additionalInsets: UIEdgeInsets(), - statusBarHeight: nil, - inputHeight: nil, - inputHeightIsInteractivellyChanging: false, - inVoiceOver: false - ), transition: .immediate) - - self.layer.addSublayer(tooltipController.view.layer) - tooltipController.viewWillAppear(false) - tooltipController.viewDidAppear(false) - tooltipController.setIgnoreAppearanceMethodInvocations(true) - tooltipController.view.isUserInteractionEnabled = false + if let tooltipController = self.tooltipController { + self.tooltipController = nil + tooltipController.view.layer.removeFromSuperlayer() + } } - transition.setFrame(view: tooltipController.view, frame: CGRect(origin: CGPoint(), size: CGSize(width: 200.0, height: 200.0)).offsetBy(dx: -200.0 * 0.5 + foregroundFrame.width + 2.0, dy: -200.0 * 0.5)) - alphaTransition.setAlpha(view: tooltipController.view, alpha: component.isExpanded ? 1.0 : 0.0) - return size } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index 794b13e9e7..2782e8b50d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -1917,7 +1917,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { let apparentBackgroundHeight = (1.0 - transitionFraction) * backgroundHeight + transitionFraction * transitionSourceHeight var subtitleRatingSize: CGSize? - if !"".isEmpty, let cachedData = cachedData as? CachedUserData, let starRating = cachedData.starRating { + if let cachedData = cachedData as? CachedUserData, let starRating = cachedData.starRating { let subtitleRating: ComponentView var subtitleRatingTransition = ComponentTransition(transition) if let current = self.subtitleRating { @@ -1978,7 +1978,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { if self.subtitleRatingIsExpanded, let controller = self.controller, let presentationData = self.presentationData, !self.didDisplayRatingTooltip { self.didDisplayRatingTooltip = true - controller.presentInGlobalOverlay(UndoOverlayController( + controller.present(UndoOverlayController( presentationData: presentationData, content: .info( title: nil, @@ -1987,10 +1987,21 @@ final class PeerInfoHeaderNode: ASDisplayNode { customUndoText: "Learn More" ), position: .top, - action: { _ in + action: { [weak self] action in + guard let self else { + return true + } + + if case .undo = action { + var infoUrl = "https://telegram.org/blog/telegram-stars" + if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["stars_rating_learnmore_url"] as? String { + infoUrl = value + } + self.context.sharedContext.applicationBindings.openUrl(infoUrl) + } return true } - )) + ), in: .current) } } )), diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift index 30ba8684d0..04d2132065 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift @@ -115,7 +115,7 @@ extension ChatControllerImpl { let textItems: [AnimatedTextComponent.Item] = [ AnimatedTextComponent.Item(id: 0, content: .text(text)) ] - let controller = UndoOverlayController(presentationData: self.presentationData, content: .starsSent(context: self.context, title: title, text: textItems), elevatedLayout: false, position: .top, action: { [weak self] action in + let controller = UndoOverlayController(presentationData: self.presentationData, content: .starsSent(context: self.context, title: title, text: textItems, hasUndo: true), elevatedLayout: false, position: .top, action: { [weak self] action in guard let self else { return false } diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift index df04ef1521..113a15b558 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift @@ -535,9 +535,9 @@ extension ChatControllerImpl { self.currentSendStarsUndoMessageId = messageId if let current = self.currentSendStarsUndoController { - current.content = .starsSent(context: self.context, title: title, text: textItems) + current.content = .starsSent(context: self.context, title: title, text: textItems, hasUndo: true) } else { - let controller = UndoOverlayController(presentationData: self.presentationData, content: .starsSent(context: self.context, title: title, text: textItems), elevatedLayout: false, position: .top, action: { [weak self] action in + let controller = UndoOverlayController(presentationData: self.presentationData, content: .starsSent(context: self.context, title: title, text: textItems, hasUndo: true), elevatedLayout: false, position: .top, action: { [weak self] action in guard let self else { return false } diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index cae06633fc..e408d91083 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -40,7 +40,7 @@ public enum UndoOverlayContent { case copy(text: String) case mediaSaved(text: String) case paymentSent(currencyValue: String, itemTitle: String) - case starsSent(context: AccountContext, title: String, text: [AnimatedTextComponent.Item]) + case starsSent(context: AccountContext, title: String, text: [AnimatedTextComponent.Item], hasUndo: Bool) case inviteRequestSent(title: String, text: String) case image(image: UIImage, title: String?, text: String, round: Bool, undoText: String?) case notificationSoundAdded(title: String, text: String, action: (() -> Void)?) diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index df4a7d6f2c..44be88e59a 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -452,7 +452,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.textNode.attributedText = string displayUndo = false self.originalRemainingSeconds = 5 - case let .starsSent(_, title, textItems): + case let .starsSent(_, title, textItems, hasUndo): self.avatarNode = nil self.iconNode = nil self.iconCheckNode = nil @@ -478,11 +478,13 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.animatedTextItems = textItems - displayUndo = true - self.originalRemainingSeconds = 4.9 + displayUndo = hasUndo + self.originalRemainingSeconds = hasUndo ? 4.9 : 3.0 isUserInteractionEnabled = true - self.statusNode = RadialStatusNode(backgroundNodeColor: .clear) + if hasUndo { + self.statusNode = RadialStatusNode(backgroundNodeColor: .clear) + } case let .messagesUnpinned(title, text, undo, isHidden): self.avatarNode = nil self.iconNode = nil @@ -1562,8 +1564,10 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { switch content { case .removedChat: self.panelWrapperNode.addSubnode(self.timerTextNode) - case .starsSent: - self.panelWrapperNode.addSubnode(self.timerTextNode) + case let .starsSent(_, _, _, hasUndo): + if hasUndo { + self.panelWrapperNode.addSubnode(self.timerTextNode) + } if self.textNode.tapAttributeAction != nil || displayUndo { self.isUserInteractionEnabled = true @@ -1835,7 +1839,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) } self.textNode.attributedText = attributedText - case let .starsSent(_, title, textItems): + case let .starsSent(_, title, textItems, _): self.animatedTextItems = textItems self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white)