From c73bceb797637a06aa720df6fb6f3efd2a753623 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Mon, 30 Mar 2026 19:59:03 +0800 Subject: [PATCH 01/50] Cleanup --- .../Sources/State/ManagedConsumePersonalMessagesActions.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/submodules/TelegramCore/Sources/State/ManagedConsumePersonalMessagesActions.swift b/submodules/TelegramCore/Sources/State/ManagedConsumePersonalMessagesActions.swift index 3c451be441..6ee67b8f45 100644 --- a/submodules/TelegramCore/Sources/State/ManagedConsumePersonalMessagesActions.swift +++ b/submodules/TelegramCore/Sources/State/ManagedConsumePersonalMessagesActions.swift @@ -480,11 +480,7 @@ private func synchronizeUnseenReactionsAndPollVotesTag(postbox: Postbox, network return postbox.transaction { transaction -> Void in transaction.replaceMessageTagSummary(peerId: entry.key.peerId, threadId: nil, tagMask: .unseenReaction, namespace: entry.key.namespace, customTag: nil, count: apiUnreadReactionsCount, maxId: apiTopMessage) - #if DEBUG - #else - //let _ = this_is_an_error transaction.replaceMessageTagSummary(peerId: entry.key.peerId, threadId: nil, tagMask: .unseenPollVote, namespace: entry.key.namespace, customTag: nil, count: apiUnreadPollVoteCount, maxId: apiTopMessage) - #endif } } else { return .complete() From 18abe71dcaf9e403c9c049c3a70ed38555f3ad25 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Mon, 30 Mar 2026 20:44:10 +0800 Subject: [PATCH 02/50] Support Xcode 26.4 --- MODULE.bazel.lock | 313 ++++++++++++++---- build-system/bazel-rules/apple_support | 2 +- build-system/bazel-rules/rules_apple | 2 +- build-system/bazel-rules/rules_swift | 2 +- build-system/bazel-rules/rules_xcodeproj | 2 +- .../AsyncDisplayKit/Source/ASDisplayNode.mm | 2 +- .../PublicHeaders/AsyncDisplayKit/ASThread.h | 6 + .../Sources/FLAnimatedImage.m | 4 +- .../Sources/GPUImageFramebuffer.m | 4 +- .../Sources/PGPhotoCustomFilterPass.m | 2 +- .../Sources/PGPhotoEditorPicture.m | 2 +- .../LegacyComponents/Sources/TGImageBlur.m | 40 +-- .../Sources/TGPhotoVideoEditor.m | 5 +- .../Sources/TGVideoCameraGLRenderer.m | 21 +- .../Sources/TGVideoCameraPipeline.m | 4 +- submodules/WebPBinding/Sources/UIImage+WebP.m | 2 +- 16 files changed, 313 insertions(+), 100 deletions(-) diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 1a563a1a8e..7cb1fb2643 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -22,11 +22,12 @@ "https://bcr.bazel.build/modules/bazel_features/1.21.0/MODULE.bazel": "675642261665d8eea09989aa3b8afb5c37627f1be178382c320d1b46afba5e3b", "https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65", "https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d", - "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.33.0/MODULE.bazel": "8b8dc9d2a4c88609409c3191165bccec0e4cb044cd7a72ccbe826583303459f6", - "https://bcr.bazel.build/modules/bazel_features/1.33.0/source.json": "13617db3930328c2cd2807a0f13d52ca870ac05f96db9668655113265147b2a6", + "https://bcr.bazel.build/modules/bazel_features/1.36.0/MODULE.bazel": "596cb62090b039caf1cad1d52a8bc35cf188ca9a4e279a828005e7ee49a1bec3", "https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7", + "https://bcr.bazel.build/modules/bazel_features/1.42.1/MODULE.bazel": "275a59b5406ff18c01739860aa70ad7ccb3cfb474579411decca11c93b951080", + "https://bcr.bazel.build/modules/bazel_features/1.42.1/source.json": "fcd4396b2df85f64f2b3bb436ad870793ecf39180f1d796f913cc9276d355309", "https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a", "https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8", "https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e", @@ -40,7 +41,9 @@ "https://bcr.bazel.build/modules/bazel_skylib/1.7.0/MODULE.bazel": "0db596f4563de7938de764cc8deeabec291f55e8ec15299718b93c4423e9796d", "https://bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel": "3120d80c5861aa616222ec015332e5f8d3171e062e3e804a2a0253e1be26e59b", "https://bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6", - "https://bcr.bazel.build/modules/bazel_skylib/1.8.1/source.json": "7ebaefba0b03efe59cac88ed5bbc67bcf59a3eff33af937345ede2a38b2d368a", + "https://bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel": "69ad6927098316848b34a9142bcc975e018ba27f08c4ff403f50c1b6e646ca67", + "https://bcr.bazel.build/modules/bazel_skylib/1.9.0/MODULE.bazel": "72997b29dfd95c3fa0d0c48322d05590418edef451f8db8db5509c57875fb4b7", + "https://bcr.bazel.build/modules/bazel_skylib/1.9.0/source.json": "7ad77c1e8c1b84222d9b3f3cae016a76639435744c19330b0b37c0a3c9da7dc0", "https://bcr.bazel.build/modules/buildozer/7.1.2/MODULE.bazel": "2e8dd40ede9c454042645fd8d8d0cd1527966aa5c919de86661e62953cd73d84", "https://bcr.bazel.build/modules/buildozer/7.1.2/source.json": "c9028a501d2db85793a6996205c8de120944f50a0d570438fcae0457a5f9d1f8", "https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb", @@ -58,23 +61,23 @@ "https://bcr.bazel.build/modules/nlohmann_json/3.12.0.bcr.1/source.json": "93f82a5ae985eb935c539bfee95e04767187818189241ac956f3ccadbdb8fb02", "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", "https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee", "https://bcr.bazel.build/modules/platforms/0.0.5/MODULE.bazel": "5733b54ea419d5eaf7997054bb55f6a1d0b5ff8aedf0176fef9eea44f3acda37", "https://bcr.bazel.build/modules/platforms/0.0.6/MODULE.bazel": "ad6eeef431dc52aefd2d77ed20a4b353f8ebf0f4ecdd26a807d2da5aa8cd0615", "https://bcr.bazel.build/modules/platforms/0.0.7/MODULE.bazel": "72fd4a0ede9ee5c021f6a8dd92b503e089f46c227ba2813ff183b71616034814", "https://bcr.bazel.build/modules/platforms/0.0.8/MODULE.bazel": "9f142c03e348f6d263719f5074b21ef3adf0b139ee4c5133e2aa35664da9eb2d", "https://bcr.bazel.build/modules/platforms/0.0.9/MODULE.bazel": "4a87a60c927b56ddd67db50c89acaa62f4ce2a1d2149ccb63ffd871d5ce29ebc", + "https://bcr.bazel.build/modules/platforms/1.0.0/MODULE.bazel": "f05feb42b48f1b3c225e4ccf351f367be0371411a803198ec34a389fb22aa580", + "https://bcr.bazel.build/modules/platforms/1.0.0/source.json": "f4ff1fd412e0246fd38c82328eb209130ead81d62dcd5a9e40910f867f733d96", "https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7", "https://bcr.bazel.build/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c", "https://bcr.bazel.build/modules/protobuf/27.1/MODULE.bazel": "703a7b614728bb06647f965264967a8ef1c39e09e8f167b3ca0bb1fd80449c0d", "https://bcr.bazel.build/modules/protobuf/29.0-rc2/MODULE.bazel": "6241d35983510143049943fc0d57937937122baf1b287862f9dc8590fc4c37df", "https://bcr.bazel.build/modules/protobuf/29.0-rc3/MODULE.bazel": "33c2dfa286578573afc55a7acaea3cada4122b9631007c594bf0729f41c8de92", "https://bcr.bazel.build/modules/protobuf/29.0/MODULE.bazel": "319dc8bf4c679ff87e71b1ccfb5a6e90a6dbc4693501d471f48662ac46d04e4e", - "https://bcr.bazel.build/modules/protobuf/29.1/MODULE.bazel": "557c3457560ff49e122ed76c0bc3397a64af9574691cb8201b4e46d4ab2ecb95", "https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0", - "https://bcr.bazel.build/modules/protobuf/33.4/MODULE.bazel": "114775b816b38b6d0ca620450d6b02550c60ceedfdc8d9a229833b34a223dc42", - "https://bcr.bazel.build/modules/protobuf/33.4/source.json": "555f8686b4c7d6b5ba731fbea13bf656b4bfd9a7ff629c1d9d3f6e1d6155de79", + "https://bcr.bazel.build/modules/protobuf/34.0.bcr.1/MODULE.bazel": "74e541b0ba877813da786a11707d4e394433c157841d5111a36be0d44b907931", + "https://bcr.bazel.build/modules/protobuf/34.0.bcr.1/source.json": "fc174b3d6215aa14197d1bd779f98bb72d9fd666ee5ec0d6bba6ae986baa4535", "https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e", "https://bcr.bazel.build/modules/pybind11_bazel/2.12.0/MODULE.bazel": "e6f4c20442eaa7c90d7190d8dc539d0ab422f95c65a57cc59562170c58ae3d34", "https://bcr.bazel.build/modules/pybind11_bazel/2.12.0/source.json": "6900fdc8a9e95866b8c0d4ad4aba4d4236317b5c1cd04c502df3f0d33afed680", @@ -96,10 +99,11 @@ "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.2/MODULE.bazel": "557ddc3a96858ec0d465a87c0a931054d7dcfd6583af2c7ed3baf494407fd8d0", + "https://bcr.bazel.build/modules/rules_cc/0.1.5/MODULE.bazel": "88dfc9361e8b5ae1008ac38f7cdfd45ad738e4fa676a3ad67d19204f045a1fd8", "https://bcr.bazel.build/modules/rules_cc/0.2.14/MODULE.bazel": "353c99ed148887ee89c54a17d4100ae7e7e436593d104b668476019023b58df8", "https://bcr.bazel.build/modules/rules_cc/0.2.15/MODULE.bazel": "6a0a4a75a57aa6dc888300d848053a58c6b12a29f89d4304e1c41448514ec6e8", - "https://bcr.bazel.build/modules/rules_cc/0.2.15/source.json": "197965c6dcca5c98a9288f93849e2e1c69d622e71b0be8deb524e22d48c88e32", + "https://bcr.bazel.build/modules/rules_cc/0.2.17/MODULE.bazel": "1849602c86cb60da8613d2de887f9566a6d354a6df6d7009f9d04a14402f9a84", + "https://bcr.bazel.build/modules/rules_cc/0.2.17/source.json": "3832f45d145354049137c0090df04629d9c2b5493dc5c2bf46f1834040133a07", "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_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74", @@ -140,23 +144,22 @@ "https://bcr.bazel.build/modules/rules_proto/6.0.0-rc1/MODULE.bazel": "1e5b502e2e1a9e825eef74476a5a1ee524a92297085015a052510b09a1a09483", "https://bcr.bazel.build/modules/rules_proto/6.0.2/MODULE.bazel": "ce916b775a62b90b61888052a416ccdda405212b6aaeb39522f7dc53431a5e73", "https://bcr.bazel.build/modules/rules_proto/7.0.2/MODULE.bazel": "bf81793bd6d2ad89a37a40693e56c61b0ee30f7a7fdbaf3eabbf5f39de47dea2", - "https://bcr.bazel.build/modules/rules_proto/7.1.0/MODULE.bazel": "002d62d9108f75bb807cd56245d45648f38275cb3a99dcd45dfb864c5d74cb96", - "https://bcr.bazel.build/modules/rules_proto/7.1.0/source.json": "39f89066c12c24097854e8f57ab8558929f9c8d474d34b2c00ac04630ad8940e", + "https://bcr.bazel.build/modules/rules_proto/7.0.2/source.json": "1e5e7260ae32ef4f2b52fd1d0de8d03b606a44c91b694d2f1afb1d3b28a48ce1", "https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f", "https://bcr.bazel.build/modules/rules_python/0.23.1/MODULE.bazel": "49ffccf0511cb8414de28321f5fcf2a31312b47c40cc21577144b7447f2bf300", "https://bcr.bazel.build/modules/rules_python/0.25.0/MODULE.bazel": "72f1506841c920a1afec76975b35312410eea3aa7b63267436bfb1dd91d2d382", - "https://bcr.bazel.build/modules/rules_python/0.27.1/MODULE.bazel": "65dc875cc1a06c30d5bbdba7ab021fd9e551a6579e408a3943a61303e2228a53", "https://bcr.bazel.build/modules/rules_python/0.28.0/MODULE.bazel": "cba2573d870babc976664a912539b320cbaa7114cd3e8f053c720171cde331ed", "https://bcr.bazel.build/modules/rules_python/0.31.0/MODULE.bazel": "93a43dc47ee570e6ec9f5779b2e64c1476a6ce921c48cc9a1678a91dd5f8fd58", "https://bcr.bazel.build/modules/rules_python/0.33.2/MODULE.bazel": "3e036c4ad8d804a4dad897d333d8dce200d943df4827cb849840055be8d2e937", "https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c", "https://bcr.bazel.build/modules/rules_python/0.40.0/MODULE.bazel": "9d1a3cd88ed7d8e39583d9ffe56ae8a244f67783ae89b60caafc9f5cf318ada7", - "https://bcr.bazel.build/modules/rules_python/1.3.0/MODULE.bazel": "8361d57eafb67c09b75bf4bbe6be360e1b8f4f18118ab48037f2bd50aa2ccb13", "https://bcr.bazel.build/modules/rules_python/1.6.0/MODULE.bazel": "7e04ad8f8d5bea40451cf80b1bd8262552aa73f841415d20db96b7241bd027d8", - "https://bcr.bazel.build/modules/rules_python/1.6.0/source.json": "e980f654cf66ec4928672f41fc66c4102b5ea54286acf4aecd23256c84211be6", + "https://bcr.bazel.build/modules/rules_python/1.7.0/MODULE.bazel": "d01f995ecd137abf30238ad9ce97f8fc3ac57289c8b24bd0bf53324d937a14f8", + "https://bcr.bazel.build/modules/rules_python/1.7.0/source.json": "028a084b65dcf8f4dc4f82f8778dbe65df133f234b316828a82e060d81bdce32", "https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c", "https://bcr.bazel.build/modules/rules_shell/0.3.0/MODULE.bazel": "de4402cd12f4cc8fda2354fce179fdb068c0b9ca1ec2d2b17b3e21b24c1a937b", - "https://bcr.bazel.build/modules/rules_shell/0.3.0/source.json": "c55ed591aa5009401ddf80ded9762ac32c358d2517ee7820be981e2de9756cf3", + "https://bcr.bazel.build/modules/rules_shell/0.6.1/MODULE.bazel": "72e76b0eea4e81611ef5452aa82b3da34caca0c8b7b5c0c9584338aa93bae26b", + "https://bcr.bazel.build/modules/rules_shell/0.6.1/source.json": "20ec05cd5e592055e214b2da8ccb283c7f2a421ea0dc2acbf1aa792e11c03d0c", "https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8", "https://bcr.bazel.build/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c", "https://bcr.bazel.build/modules/stardoc/0.5.6/MODULE.bazel": "c43dabc564990eeab55e25ed61c07a1aadafe9ece96a4efabb3f8bf9063b71ef", @@ -164,8 +167,8 @@ "https://bcr.bazel.build/modules/stardoc/0.7.1/MODULE.bazel": "3548faea4ee5dda5580f9af150e79d0f6aea934fc60c1cc50f4efdd9420759e7", "https://bcr.bazel.build/modules/stardoc/0.7.2/MODULE.bazel": "fc152419aa2ea0f51c29583fab1e8c99ddefd5b3778421845606ee628629e0e5", "https://bcr.bazel.build/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216", - "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.2/MODULE.bazel": "75aab2373a4bbe2a1260b9bf2a1ebbdbf872d3bd36f80bff058dccd82e89422f", - "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.2/source.json": "5fba48bbe0ba48761f9e9f75f92876cafb5d07c0ce059cc7a8027416de94a05b", + "https://bcr.bazel.build/modules/swift_argument_parser/1.7.0/MODULE.bazel": "40d4e44950e44973dcf8590bcee637591de196b5dbe3696d07eb342b74d53672", + "https://bcr.bazel.build/modules/swift_argument_parser/1.7.0/source.json": "b9b952cba0c748083b9b891e6ac46d347c92d37e8a92ead96d2a54b966bacd87", "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", "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", @@ -238,10 +241,245 @@ ] } }, + "@@rules_python+//python/extensions:config.bzl%config": { + "general": { + "bzlTransitiveDigest": "xaCns8Qt+8bJqVLy8r6nc/eL2AjEIX/vOdjqoh5xYac=", + "usagesDigest": "ZVSXMAGpD+xzVNPuvF1IoLBkty7TROO0+akMapt1pAg=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "rules_python_internal": { + "repoRuleId": "@@rules_python+//python/private:internal_config_repo.bzl%internal_config_repo", + "attributes": { + "transition_setting_generators": {}, + "transition_settings": [] + } + }, + "pypi__build": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/e2/03/f3c8ba0a6b6e30d7d18c40faab90807c9bb5e9a1e3b2fe2008af624a9c97/build-1.2.1-py3-none-any.whl", + "sha256": "75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__click": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", + "sha256": "ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__colorama": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", + "sha256": "4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__importlib_metadata": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/2d/0a/679461c511447ffaf176567d5c496d1de27cbe34a87df6677d7171b2fbd4/importlib_metadata-7.1.0-py3-none-any.whl", + "sha256": "30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__installer": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/e5/ca/1172b6638d52f2d6caa2dd262ec4c811ba59eee96d54a7701930726bce18/installer-0.7.0-py3-none-any.whl", + "sha256": "05d1933f0a5ba7d8d6296bb6d5018e7c94fa473ceb10cf198a92ccea19c27b53", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__more_itertools": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/50/e2/8e10e465ee3987bb7c9ab69efb91d867d93959095f4807db102d07995d94/more_itertools-10.2.0-py3-none-any.whl", + "sha256": "686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__packaging": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl", + "sha256": "2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__pep517": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/25/6e/ca4a5434eb0e502210f591b97537d322546e4833dcb4d470a48c375c5540/pep517-0.13.1-py3-none-any.whl", + "sha256": "31b206f67165b3536dd577c5c3f1518e8fbaf38cbc57efff8369a392feff1721", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__pip": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/8a/6a/19e9fe04fca059ccf770861c7d5721ab4c2aebc539889e97c7977528a53b/pip-24.0-py3-none-any.whl", + "sha256": "ba0d021a166865d2265246961bec0152ff124de910c5cc39f1156ce3fa7c69dc", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__pip_tools": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/0d/dc/38f4ce065e92c66f058ea7a368a9c5de4e702272b479c0992059f7693941/pip_tools-7.4.1-py3-none-any.whl", + "sha256": "4c690e5fbae2f21e87843e89c26191f0d9454f362d8acdbd695716493ec8b3a9", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__pyproject_hooks": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/ae/f3/431b9d5fe7d14af7a32340792ef43b8a714e7726f1d7b69cc4e8e7a3f1d7/pyproject_hooks-1.1.0-py3-none-any.whl", + "sha256": "7ceeefe9aec63a1064c18d939bdc3adf2d8aa1988a510afec15151578b232aa2", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__setuptools": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/90/99/158ad0609729111163fc1f674a5a42f2605371a4cf036d0441070e2f7455/setuptools-78.1.1-py3-none-any.whl", + "sha256": "c3a9c4211ff4c309edb8b8c4f1cbfa7ae324c4ba9f91ff254e3d305b9fd54561", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__tomli": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", + "sha256": "939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__wheel": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/7d/cd/d7460c9a869b16c3dd4e1e403cce337df165368c71d6af229a74699622ce/wheel-0.43.0-py3-none-any.whl", + "sha256": "55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__zipp": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/da/55/a03fd7240714916507e1fcf7ae355bd9d9ed2e6db492595f1a67f61681be/zipp-3.18.2-py3-none-any.whl", + "sha256": "dce197b859eb796242b0622af1b8beb0a722d52aa2f57133ead08edd5bf5374e", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + } + }, + "recordedRepoMappingEntries": [ + [ + "rules_python+", + "bazel_tools", + "bazel_tools" + ], + [ + "rules_python+", + "pypi__build", + "rules_python++config+pypi__build" + ], + [ + "rules_python+", + "pypi__click", + "rules_python++config+pypi__click" + ], + [ + "rules_python+", + "pypi__colorama", + "rules_python++config+pypi__colorama" + ], + [ + "rules_python+", + "pypi__importlib_metadata", + "rules_python++config+pypi__importlib_metadata" + ], + [ + "rules_python+", + "pypi__installer", + "rules_python++config+pypi__installer" + ], + [ + "rules_python+", + "pypi__more_itertools", + "rules_python++config+pypi__more_itertools" + ], + [ + "rules_python+", + "pypi__packaging", + "rules_python++config+pypi__packaging" + ], + [ + "rules_python+", + "pypi__pep517", + "rules_python++config+pypi__pep517" + ], + [ + "rules_python+", + "pypi__pip", + "rules_python++config+pypi__pip" + ], + [ + "rules_python+", + "pypi__pip_tools", + "rules_python++config+pypi__pip_tools" + ], + [ + "rules_python+", + "pypi__pyproject_hooks", + "rules_python++config+pypi__pyproject_hooks" + ], + [ + "rules_python+", + "pypi__setuptools", + "rules_python++config+pypi__setuptools" + ], + [ + "rules_python+", + "pypi__tomli", + "rules_python++config+pypi__tomli" + ], + [ + "rules_python+", + "pypi__wheel", + "rules_python++config+pypi__wheel" + ], + [ + "rules_python+", + "pypi__zipp", + "rules_python++config+pypi__zipp" + ] + ] + } + }, "@@rules_python+//python/uv:uv.bzl%uv": { "general": { - "bzlTransitiveDigest": "PmZM/pIkZKEDDL68TohlKJrWPYKL5VwUw3MA7kmm6fk=", - "usagesDigest": "p80sy6cYQuWxx5jhV3fOTu+N9EyIUFG9+F7UC/nhXic=", + "bzlTransitiveDigest": "N8SCcKcL6KnzBLApxvY2jR9vhXjA2VCBZMLZfY3sDRA=", + "usagesDigest": "H8dQoNZcoqP+Mu0tHZTi4KHATzvNkM5ePuEqoQdklIU=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, @@ -278,43 +516,6 @@ ] ] } - }, - "@@rules_xcodeproj+//xcodeproj:extensions.bzl%internal": { - "general": { - "bzlTransitiveDigest": "+qmLBZzimJ0CYyKoQg6/pbdkTnu/s4e5IisoM+TLM+8=", - "usagesDigest": "fvsnMonVwKDYnBfww4bXuYie3WU0d9VSqT2gePSdQco=", - "recordedFileInputs": {}, - "recordedDirentsInputs": {}, - "envVariables": {}, - "generatedRepoSpecs": { - "rules_xcodeproj_generated": { - "repoRuleId": "@@rules_xcodeproj+//xcodeproj:repositories.bzl%generated_files_repo", - "attributes": {} - } - }, - "recordedRepoMappingEntries": [ - [ - "bazel_features+", - "bazel_features_globals", - "bazel_features++version_extension+bazel_features_globals" - ], - [ - "bazel_features+", - "bazel_features_version", - "bazel_features++version_extension+bazel_features_version" - ], - [ - "rules_xcodeproj+", - "bazel_features", - "bazel_features+" - ], - [ - "rules_xcodeproj+", - "bazel_tools", - "bazel_tools" - ] - ] - } } } } diff --git a/build-system/bazel-rules/apple_support b/build-system/bazel-rules/apple_support index 7967e453d1..a99414ad84 160000 --- a/build-system/bazel-rules/apple_support +++ b/build-system/bazel-rules/apple_support @@ -1 +1 @@ -Subproject commit 7967e453d1f54963935a1ae06808e1c64420bfb1 +Subproject commit a99414ad848c3aeb84640934352ecc85d8a937f5 diff --git a/build-system/bazel-rules/rules_apple b/build-system/bazel-rules/rules_apple index f6aef7db5b..1791d916de 160000 --- a/build-system/bazel-rules/rules_apple +++ b/build-system/bazel-rules/rules_apple @@ -1 +1 @@ -Subproject commit f6aef7db5b649ffc3b7497ecc83a08ad3ab19559 +Subproject commit 1791d916de4083388f22e20248d8b010d23f0d6b diff --git a/build-system/bazel-rules/rules_swift b/build-system/bazel-rules/rules_swift index 2b9261134a..9dce728ed1 160000 --- a/build-system/bazel-rules/rules_swift +++ b/build-system/bazel-rules/rules_swift @@ -1 +1 @@ -Subproject commit 2b9261134a123ad83f8b1d2dc7fc69ae83fe7172 +Subproject commit 9dce728ed1e9168ec8c912fcd3443dad48a286fe diff --git a/build-system/bazel-rules/rules_xcodeproj b/build-system/bazel-rules/rules_xcodeproj index 79f26ec131..997f2db058 160000 --- a/build-system/bazel-rules/rules_xcodeproj +++ b/build-system/bazel-rules/rules_xcodeproj @@ -1 +1 @@ -Subproject commit 79f26ec1315531a8459fe95934440cb3ca3c16df +Subproject commit 997f2db058596f91663e54782b79490de87208da diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNode.mm b/submodules/AsyncDisplayKit/Source/ASDisplayNode.mm index 20e770645e..7a972fd335 100644 --- a/submodules/AsyncDisplayKit/Source/ASDisplayNode.mm +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNode.mm @@ -3450,7 +3450,7 @@ ASDISPLAYNODE_INLINE BOOL subtreeIsRasterized(ASDisplayNode *node) { - (ASDisplayNodePerformanceMeasurements)performanceMeasurements { MutexLocker l(__instanceLock__); - ASDisplayNodePerformanceMeasurements measurements = { .layoutSpecNumberOfPasses = -1, .layoutSpecTotalTime = NAN, .layoutComputationNumberOfPasses = -1, .layoutComputationTotalTime = NAN }; + ASDisplayNodePerformanceMeasurements measurements = { .layoutSpecTotalTime = NAN, .layoutSpecNumberOfPasses = -1, .layoutComputationTotalTime = NAN, .layoutComputationNumberOfPasses = -1 }; if (_measurementOptions & ASDisplayNodePerformanceMeasurementOptionLayoutSpec) { measurements.layoutSpecNumberOfPasses = _layoutSpecNumberOfPasses; measurements.layoutSpecTotalTime = _layoutSpecTotalTime; diff --git a/submodules/AsyncDisplayKit/Source/PublicHeaders/AsyncDisplayKit/ASThread.h b/submodules/AsyncDisplayKit/Source/PublicHeaders/AsyncDisplayKit/ASThread.h index 94b2a28140..fd30747d2f 100644 --- a/submodules/AsyncDisplayKit/Source/PublicHeaders/AsyncDisplayKit/ASThread.h +++ b/submodules/AsyncDisplayKit/Source/PublicHeaders/AsyncDisplayKit/ASThread.h @@ -163,6 +163,8 @@ namespace AS { return success; } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wthread-safety-analysis" void lock() { switch (_type) { case Plain: @@ -184,7 +186,10 @@ namespace AS { } DidLock(); } +#pragma clang diagnostic pop +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wthread-safety-analysis" void unlock() { WillUnlock(); switch (_type) { @@ -206,6 +211,7 @@ namespace AS { break; } } +#pragma clang diagnostic pop void AssertHeld() { ASDisplayNodeCAssert(_owner == std::this_thread::get_id(), @"Thread should hold lock"); diff --git a/submodules/LegacyComponents/Sources/FLAnimatedImage.m b/submodules/LegacyComponents/Sources/FLAnimatedImage.m index dd3014c232..95eded37c1 100644 --- a/submodules/LegacyComponents/Sources/FLAnimatedImage.m +++ b/submodules/LegacyComponents/Sources/FLAnimatedImage.m @@ -291,9 +291,9 @@ typedef NS_ENUM(NSUInteger, FLAnimatedImageFrameCacheSize) { // Calculate the optimal frame cache size: try choosing a larger buffer window depending on the predicted image size. // It's only dependent on the image size & number of frames and never changes. CGFloat animatedImageDataSize = CGImageGetBytesPerRow(self.posterImage.CGImage) * self.size.height * self.frameCount / MEGABYTE; - if (animatedImageDataSize <= FLAnimatedImageDataSizeCategoryAll) { + if (animatedImageDataSize <= (double)FLAnimatedImageDataSizeCategoryAll) { _frameCacheSizeOptimal = self.frameCount; - } else if (animatedImageDataSize <= FLAnimatedImageDataSizeCategoryDefault) { + } else if (animatedImageDataSize <= (double)FLAnimatedImageDataSizeCategoryDefault) { // This value doesn't depend on device memory much because if we're not keeping all frames in memory we will always be decoding 1 frame up ahead per 1 frame that gets played and at this point we might as well just keep a small buffer just large enough to keep from running out of frames. _frameCacheSizeOptimal = FLAnimatedImageFrameCacheSizeDefault; } else { diff --git a/submodules/LegacyComponents/Sources/GPUImageFramebuffer.m b/submodules/LegacyComponents/Sources/GPUImageFramebuffer.m index 13f4b82182..c3de61b794 100755 --- a/submodules/LegacyComponents/Sources/GPUImageFramebuffer.m +++ b/submodules/LegacyComponents/Sources/GPUImageFramebuffer.m @@ -381,11 +381,11 @@ void dataProviderUnlockCallback (void *info, __unused const void *data, __unused if ([GPUImageContext supportsFastTextureUpload]) { - cgImageFromBytes = CGImageCreate((int)_size.width, (int)_size.height, 8, 32, CVPixelBufferGetBytesPerRow(renderTarget), defaultRGBColorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst, dataProvider, NULL, NO, kCGRenderingIntentDefault); + cgImageFromBytes = CGImageCreate((int)_size.width, (int)_size.height, 8, 32, CVPixelBufferGetBytesPerRow(renderTarget), defaultRGBColorSpace, ((uint32_t)kCGBitmapByteOrder32Little) | ((uint32_t)kCGImageAlphaPremultipliedFirst), dataProvider, NULL, NO, kCGRenderingIntentDefault); } else { - cgImageFromBytes = CGImageCreate((int)_size.width, (int)_size.height, 8, 32, 4 * (int)_size.width, defaultRGBColorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedLast, dataProvider, NULL, NO, kCGRenderingIntentDefault); + cgImageFromBytes = CGImageCreate((int)_size.width, (int)_size.height, 8, 32, 4 * (int)_size.width, defaultRGBColorSpace, ((uint32_t)kCGBitmapByteOrderDefault) | ((uint32_t)kCGImageAlphaPremultipliedLast), dataProvider, NULL, NO, kCGRenderingIntentDefault); } // Capture image with current device orientation diff --git a/submodules/LegacyComponents/Sources/PGPhotoCustomFilterPass.m b/submodules/LegacyComponents/Sources/PGPhotoCustomFilterPass.m index 830ff3a367..97a1c3b63a 100644 --- a/submodules/LegacyComponents/Sources/PGPhotoCustomFilterPass.m +++ b/submodules/LegacyComponents/Sources/PGPhotoCustomFilterPass.m @@ -150,7 +150,7 @@ NSString *const PGPhotoFilterMainShaderString = PGShaderString CGColorSpaceRef genericRGBColorspace = CGColorSpaceCreateDeviceRGB(); - CGContextRef imageContext = CGBitmapContextCreate(imageData, (size_t)imageSize.width, (size_t)imageSize.height, 8, (size_t)imageSize.width * 4, genericRGBColorspace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst); + CGContextRef imageContext = CGBitmapContextCreate(imageData, (size_t)imageSize.width, (size_t)imageSize.height, 8, (size_t)imageSize.width * 4, genericRGBColorspace, ((uint32_t)kCGBitmapByteOrder32Little) | ((uint32_t)kCGImageAlphaPremultipliedFirst)); CGSize imageDrawSize = CGSizeMake(imageSize.width, imageSize.height); if (image.imageOrientation == UIImageOrientationLeft || image.imageOrientation == UIImageOrientationRight) diff --git a/submodules/LegacyComponents/Sources/PGPhotoEditorPicture.m b/submodules/LegacyComponents/Sources/PGPhotoEditorPicture.m index 399acd45c3..68d2367074 100644 --- a/submodules/LegacyComponents/Sources/PGPhotoEditorPicture.m +++ b/submodules/LegacyComponents/Sources/PGPhotoEditorPicture.m @@ -114,7 +114,7 @@ CGColorSpaceRef genericRGBColorspace = CGColorSpaceCreateDeviceRGB(); - CGContextRef imageContext = CGBitmapContextCreate(imageData, (size_t)imageSize.width, (size_t)imageSize.height, 8, (size_t)imageSize.width * 4, genericRGBColorspace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst); + CGContextRef imageContext = CGBitmapContextCreate(imageData, (size_t)imageSize.width, (size_t)imageSize.height, 8, (size_t)imageSize.width * 4, genericRGBColorspace, ((uint32_t)kCGBitmapByteOrder32Little) | ((uint32_t)kCGImageAlphaPremultipliedFirst)); CGSize imageDrawSize = CGSizeMake(imageSize.width, imageSize.height); if (orientation == UIImageOrientationLeft || orientation == UIImageOrientationRight) diff --git a/submodules/LegacyComponents/Sources/TGImageBlur.m b/submodules/LegacyComponents/Sources/TGImageBlur.m index 015ff9db34..20d6e59fa7 100644 --- a/submodules/LegacyComponents/Sources/TGImageBlur.m +++ b/submodules/LegacyComponents/Sources/TGImageBlur.m @@ -492,7 +492,7 @@ static void addAttachmentImageCorners(void *memory, const unsigned int width, co defaultAlphaMemory = malloc(contextStride * contextHeight); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; + CGBitmapInfo bitmapInfo = ((uint32_t)kCGImageAlphaPremultipliedFirst) | ((uint32_t)kCGBitmapByteOrder32Host); CGContextRef targetContext = CGBitmapContextCreate(defaultContextMemory, contextWidth, contextHeight, 8, contextStride, colorSpace, bitmapInfo); CFRelease(colorSpace); @@ -534,7 +534,7 @@ static void addAttachmentImageCorners(void *memory, const unsigned int width, co alphaMemory = malloc(contextStride * contextHeight); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; + CGBitmapInfo bitmapInfo = ((uint32_t)kCGImageAlphaPremultipliedFirst) | ((uint32_t)kCGBitmapByteOrder32Host); CGContextRef targetContext = CGBitmapContextCreate(contextMemory, contextWidth, contextHeight, 8, contextStride, colorSpace, bitmapInfo); CFRelease(colorSpace); @@ -774,7 +774,7 @@ void TGAddImageCorners(void *memory, const unsigned int width, const unsigned in alphaMemory = malloc(contextStride * contextHeight); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; + CGBitmapInfo bitmapInfo = ((uint32_t)kCGImageAlphaPremultipliedFirst) | ((uint32_t)kCGBitmapByteOrder32Host); CGContextRef targetContext = CGBitmapContextCreate(contextMemory, contextWidth, contextHeight, 8, contextStride, colorSpace, bitmapInfo); CFRelease(colorSpace); @@ -1053,7 +1053,7 @@ UIImage *TGAverageColorAttachmentWithCornerRadiusImage(UIColor *color, bool atta void *targetMemory = malloc((int)(targetBytesPerRow * targetContextSize.height)); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; + CGBitmapInfo bitmapInfo = ((uint32_t)kCGImageAlphaPremultipliedFirst) | ((uint32_t)kCGBitmapByteOrder32Host); CGContextRef targetContext = CGBitmapContextCreate(targetMemory, (int)targetContextSize.width, (int)targetContextSize.height, 8, targetBytesPerRow, colorSpace, bitmapInfo); @@ -1180,7 +1180,7 @@ TGStaticBackdropAreaData *createImageBackdropArea(uint8_t *sourceImageMemory, in memset(memory, 0x00, (int)(bytesPerRow * contextSize.height)); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; + CGBitmapInfo bitmapInfo = ((uint32_t)kCGImageAlphaPremultipliedFirst) | ((uint32_t)kCGBitmapByteOrder32Host); CGContextRef context = CGBitmapContextCreate(memory, (int)contextSize.width, (int)contextSize.height, 8, bytesPerRow, colorSpace, bitmapInfo); CGColorSpaceRelease(colorSpace); UIGraphicsPushContext(context); @@ -1284,7 +1284,7 @@ UIImage *TGBlurredAttachmentWithCornerRadiusImage(UIImage *source, CGSize size, void *actionCircleMemory = malloc(((int)(actionCircleBytesPerRow * actionCircleContextSize.height))); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; + CGBitmapInfo bitmapInfo = ((uint32_t)kCGImageAlphaPremultipliedFirst) | ((uint32_t)kCGBitmapByteOrder32Host); CGContextRef blurredContext = CGBitmapContextCreate(blurredMemory, (int)blurredContextSize.width, (int)blurredContextSize.height, 8, blurredBytesPerRow, colorSpace, bitmapInfo); CGContextRef targetContext = CGBitmapContextCreate(targetMemory, (int)targetContextSize.width, (int)targetContextSize.height, 8, targetBytesPerRow, colorSpace, bitmapInfo); @@ -1416,7 +1416,7 @@ UIImage *TGSecretBlurredAttachmentWithCornerRadiusImage(UIImage *source, CGSize void *actionCircleMemory = malloc(((int)(actionCircleBytesPerRow * actionCircleContextSize.height))); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; + CGBitmapInfo bitmapInfo = ((uint32_t)kCGImageAlphaPremultipliedFirst) | ((uint32_t)kCGBitmapByteOrder32Host); CGContextRef blurredContext = CGBitmapContextCreate(blurredMemory, (int)blurredContextSize.width, (int)blurredContextSize.height, 8, blurredBytesPerRow, colorSpace, bitmapInfo); CGContextRef targetContext = CGBitmapContextCreate(targetMemory, (int)targetContextSize.width, (int)targetContextSize.height, 8, targetBytesPerRow, colorSpace, bitmapInfo); @@ -1546,7 +1546,7 @@ UIImage *TGBlurredFileImage(UIImage *source, CGSize size, uint32_t *averageColor void *actionCircleMemory = malloc(((int)(actionCircleBytesPerRow * actionCircleContextSize.height))); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; + CGBitmapInfo bitmapInfo = ((uint32_t)kCGImageAlphaPremultipliedFirst) | ((uint32_t)kCGBitmapByteOrder32Host); CGContextRef blurredContext = CGBitmapContextCreate(blurredMemory, (int)blurredContextSize.width, (int)blurredContextSize.height, 8, blurredBytesPerRow, colorSpace, bitmapInfo); CGContextRef targetContext = CGBitmapContextCreate(targetMemory, (int)targetContextSize.width, (int)targetContextSize.height, 8, targetBytesPerRow, colorSpace, bitmapInfo); @@ -1657,7 +1657,7 @@ UIImage *TGBlurredAlphaImage(UIImage *source, CGSize size) void *targetMemory = malloc((int)(targetBytesPerRow * targetContextSize.height)); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; + CGBitmapInfo bitmapInfo = ((uint32_t)kCGImageAlphaPremultipliedFirst) | ((uint32_t)kCGBitmapByteOrder32Host); CGContextRef blurredContext = CGBitmapContextCreate(blurredMemory, (int)blurredContextSize.width, (int)blurredContextSize.height, 8, blurredBytesPerRow, colorSpace, bitmapInfo); CGContextRef targetContext = CGBitmapContextCreate(targetMemory, (int)targetContextSize.width, (int)targetContextSize.height, 8, targetBytesPerRow, colorSpace, bitmapInfo); @@ -1733,7 +1733,7 @@ UIImage *TGBlurredRectangularImage(UIImage *source, bool more, CGSize size, CGSi void *targetMemory = malloc((int)(targetBytesPerRow * targetContextSize.height)); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; + CGBitmapInfo bitmapInfo = ((uint32_t)kCGImageAlphaPremultipliedFirst) | ((uint32_t)kCGBitmapByteOrder32Host); CGContextRef blurredContext = CGBitmapContextCreate(blurredMemory, (int)blurredContextSize.width, (int)blurredContextSize.height, 8, blurredBytesPerRow, colorSpace, bitmapInfo); CGContextRef targetContext = CGBitmapContextCreate(targetMemory, (int)targetContextSize.width, (int)targetContextSize.height, 8, targetBytesPerRow, colorSpace, bitmapInfo); @@ -1815,7 +1815,7 @@ UIImage *TGLoadedAttachmentWithCornerRadiusImage(UIImage *source, CGSize size, u void *actionCircleMemory = malloc(((int)(actionCircleBytesPerRow * actionCircleContextSize.height))); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; + CGBitmapInfo bitmapInfo = ((uint32_t)kCGImageAlphaPremultipliedFirst) | ((uint32_t)kCGBitmapByteOrder32Host); CGContextRef targetContext = CGBitmapContextCreate(targetMemory, (int)targetContextSize.width, (int)targetContextSize.height, 8, targetBytesPerRow, colorSpace, bitmapInfo); CGContextRef actionCircleContext = CGBitmapContextCreate(actionCircleMemory, (int)actionCircleContextSize.width, (int)actionCircleContextSize.height, 8, actionCircleBytesPerRow, colorSpace, bitmapInfo); @@ -1907,7 +1907,7 @@ UIImage *TGAnimationFrameAttachmentImage(UIImage *source, CGSize size, CGSize re void *targetMemory = malloc((int)(targetBytesPerRow * targetContextSize.height)); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; + CGBitmapInfo bitmapInfo = ((uint32_t)kCGImageAlphaPremultipliedFirst) | ((uint32_t)kCGBitmapByteOrder32Host); CGContextRef targetContext = CGBitmapContextCreate(targetMemory, (int)targetContextSize.width, (int)targetContextSize.height, 8, targetBytesPerRow, colorSpace, bitmapInfo); @@ -1954,7 +1954,7 @@ UIImage *TGLoadedFileImage(UIImage *source, CGSize size, uint32_t *averageColor, memset(actionCircleMemory, 0xff, actionCircleBytesPerRow * actionCircleContextSize.height); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; + CGBitmapInfo bitmapInfo = ((uint32_t)kCGImageAlphaPremultipliedFirst) | ((uint32_t)kCGBitmapByteOrder32Host); CGContextRef targetContext = CGBitmapContextCreate(targetMemory, (int)targetContextSize.width, (int)targetContextSize.height, 8, targetBytesPerRow, colorSpace, bitmapInfo); CGContextRef actionCircleContext = CGBitmapContextCreate(actionCircleMemory, (int)actionCircleContextSize.width, (int)actionCircleContextSize.height, 8, actionCircleBytesPerRow, colorSpace, bitmapInfo); @@ -2048,7 +2048,7 @@ UIImage *TGReducedAttachmentWithCornerRadiusImage(UIImage *source, CGSize origin void *targetMemory = malloc((int)(targetBytesPerRow * targetContextSize.height)); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; + CGBitmapInfo bitmapInfo = ((uint32_t)kCGImageAlphaPremultipliedFirst) | ((uint32_t)kCGBitmapByteOrder32Host); CGContextRef targetContext = CGBitmapContextCreate(targetMemory, (int)targetContextSize.width, (int)targetContextSize.height, 8, targetBytesPerRow, colorSpace, bitmapInfo); CGColorSpaceRelease(colorSpace); @@ -2137,7 +2137,7 @@ UIImage *TGBlurredBackgroundImage(UIImage *source, CGSize size) void *targetMemory = malloc((int)(targetBytesPerRow * targetContextSize.height)); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; + CGBitmapInfo bitmapInfo = ((uint32_t)kCGImageAlphaPremultipliedFirst) | ((uint32_t)kCGBitmapByteOrder32Host); CGContextRef targetContext = CGBitmapContextCreate(targetMemory, (int)targetContextSize.width, (int)targetContextSize.height, 8, targetBytesPerRow, colorSpace, bitmapInfo); @@ -2173,7 +2173,7 @@ UIImage *TGRoundImage(UIImage *source, CGSize size) memset(targetMemory, 0, (int)(targetBytesPerRow * targetContextSize.height)); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; + CGBitmapInfo bitmapInfo = ((uint32_t)kCGImageAlphaPremultipliedFirst) | ((uint32_t)kCGBitmapByteOrder32Host); CGContextRef targetContext = CGBitmapContextCreate(targetMemory, (int)targetContextSize.width, (int)targetContextSize.height, 8, targetBytesPerRow, colorSpace, bitmapInfo); @@ -2213,7 +2213,7 @@ void TGPlainImageAverageColor(UIImage *source, uint32_t *averageColor) void *targetMemory = malloc((int)(targetBytesPerRow * targetContextSize.height)); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; + CGBitmapInfo bitmapInfo = ((uint32_t)kCGImageAlphaPremultipliedFirst) | ((uint32_t)kCGBitmapByteOrder32Host); CGContextRef targetContext = CGBitmapContextCreate(targetMemory, (int)targetContextSize.width, (int)targetContextSize.height, 8, targetBytesPerRow, colorSpace, bitmapInfo); @@ -2272,7 +2272,7 @@ UIImage *TGCropBackdropImage(UIImage *source, CGSize size) void *targetMemory = malloc((int)(targetBytesPerRow * targetContextSize.height)); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; + CGBitmapInfo bitmapInfo = ((uint32_t)kCGImageAlphaPremultipliedFirst) | ((uint32_t)kCGBitmapByteOrder32Host); CGContextRef targetContext = CGBitmapContextCreate(targetMemory, (int)targetContextSize.width, (int)targetContextSize.height, 8, targetBytesPerRow, colorSpace, bitmapInfo); @@ -2320,7 +2320,7 @@ UIImage *TGScaleAndCropImageToPixelSize(UIImage *source, CGSize size, CGSize ren void *targetMemory = malloc((int)(targetBytesPerRow * targetContextSize.height)); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; + CGBitmapInfo bitmapInfo = ((uint32_t)kCGImageAlphaPremultipliedFirst) | ((uint32_t)kCGBitmapByteOrder32Host); CGContextRef targetContext = CGBitmapContextCreate(targetMemory, (int)targetContextSize.width, (int)targetContextSize.height, 8, targetBytesPerRow, colorSpace, bitmapInfo); @@ -2365,7 +2365,7 @@ NSArray *TGBlurredBackgroundImages(UIImage *source, CGSize sourceSize) void *targetMemory = malloc((int)(targetBytesPerRow * targetContextSize.height)); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; + CGBitmapInfo bitmapInfo = ((uint32_t)kCGImageAlphaPremultipliedFirst) | ((uint32_t)kCGBitmapByteOrder32Host); CGContextRef targetContext = CGBitmapContextCreate(targetMemory, (int)targetContextSize.width, (int)targetContextSize.height, 8, targetBytesPerRow, colorSpace, bitmapInfo); diff --git a/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m b/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m index 9aa57316db..4f81c2f1fe 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m +++ b/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m @@ -126,9 +126,10 @@ transitionView.alpha = 1.0; dispatch_async(dispatch_get_main_queue(), ^ { - if (completion != nil) + if (completion != nil) { completion(); - dismissed(); + } + dismissed(); }); }]; }; diff --git a/submodules/LegacyComponents/Sources/TGVideoCameraGLRenderer.m b/submodules/LegacyComponents/Sources/TGVideoCameraGLRenderer.m index 603d82cd83..33f3ec41ce 100644 --- a/submodules/LegacyComponents/Sources/TGVideoCameraGLRenderer.m +++ b/submodules/LegacyComponents/Sources/TGVideoCameraGLRenderer.m @@ -396,24 +396,29 @@ glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); glBindTexture(CVOpenGLESTextureGetTarget(srcTexture), 0); - if (hasPreviousTexture) + if (hasPreviousTexture) { glBindTexture(CVOpenGLESTextureGetTarget(prevTexture), 0); + } glBindTexture(CVOpenGLESTextureGetTarget(dstTexture), 0); glFlush(); bail: - if (oldContext != _context) - [EAGLContext setCurrentContext:oldContext]; + if (oldContext != _context) { + [EAGLContext setCurrentContext:oldContext]; + } - if (srcTexture) - CFRelease(srcTexture); + if (srcTexture) { + CFRelease(srcTexture); + } - if (prevTexture) + if (prevTexture) { CFRelease(prevTexture); + } - if (dstTexture) - CFRelease(dstTexture); + if (dstTexture) { + CFRelease(dstTexture); + } return dstPixelBuffer; } diff --git a/submodules/LegacyComponents/Sources/TGVideoCameraPipeline.m b/submodules/LegacyComponents/Sources/TGVideoCameraPipeline.m index 5afa14bbe8..8ef58642c0 100644 --- a/submodules/LegacyComponents/Sources/TGVideoCameraPipeline.m +++ b/submodules/LegacyComponents/Sources/TGVideoCameraPipeline.m @@ -453,7 +453,7 @@ const NSInteger TGVideoCameraRetainedBufferCount = 16; CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGContextRef context = CGBitmapContextCreate(baseAddress, width, height, 8, bytesPerRow, colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst); + CGContextRef context = CGBitmapContextCreate(baseAddress, width, height, 8, bytesPerRow, colorSpace, ((uint32_t)kCGBitmapByteOrder32Little) | ((uint32_t)kCGImageAlphaPremultipliedFirst)); CGImageRef cgImage = CGBitmapContextCreateImage(context); CVPixelBufferUnlockBaseAddress(imageBuffer.buffer, 0); @@ -633,7 +633,7 @@ const NSInteger TGVideoCameraRetainedBufferCount = 16; inBuff.data = baseAddress + startpos; CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGContextRef context = CGBitmapContextCreateWithData(NULL, outWidth, outHeight, 8, outWidth * 4, colorSpace, kCGImageByteOrder32Little | kCGImageAlphaPremultipliedFirst, NULL, nil); + CGContextRef context = CGBitmapContextCreateWithData(NULL, outWidth, outHeight, 8, outWidth * 4, colorSpace, ((uint32_t)kCGImageByteOrder32Little) | ((uint32_t)kCGImageAlphaPremultipliedFirst), NULL, nil); unsigned char *outImg = CGBitmapContextGetData(context); vImage_Buffer outBuff = {outImg, outHeight, outWidth, 4 * outWidth}; diff --git a/submodules/WebPBinding/Sources/UIImage+WebP.m b/submodules/WebPBinding/Sources/UIImage+WebP.m index c6a1d9d371..cb63b00660 100644 --- a/submodules/WebPBinding/Sources/UIImage+WebP.m +++ b/submodules/WebPBinding/Sources/UIImage+WebP.m @@ -24,7 +24,7 @@ void *targetMemory = malloc((int)(targetBytesPerRow * targetContextSize.height)); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; + CGBitmapInfo bitmapInfo = ((uint32_t)kCGImageAlphaPremultipliedFirst) | ((uint32_t)kCGBitmapByteOrder32Host); CGContextRef targetContext = CGBitmapContextCreate(targetMemory, (int)targetContextSize.width, (int)targetContextSize.height, 8, targetBytesPerRow, colorSpace, bitmapInfo); From 7ea47b8105b5fa9d73ace3eb4e402c2db818922a Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 30 Mar 2026 17:23:39 +0200 Subject: [PATCH 03/50] Various fixes --- .../GalleryData/Sources/GalleryData.swift | 29 +++++++++++++++---- .../GalleryUI/Sources/GalleryController.swift | 13 +++++++-- .../Items/UniversalVideoGalleryItem.swift | 4 ++- .../MediaPlaybackHeaderPanelComponent.swift | 6 ++-- .../ChatControllerOpenPollOptionMedia.swift | 17 ++--------- .../OverlayAudioPlayerControllerNode.swift | 25 ++++++++++++++-- 6 files changed, 66 insertions(+), 28 deletions(-) diff --git a/submodules/GalleryData/Sources/GalleryData.swift b/submodules/GalleryData/Sources/GalleryData.swift index f948ee945c..91ed759a29 100644 --- a/submodules/GalleryData/Sources/GalleryData.swift +++ b/submodules/GalleryData/Sources/GalleryData.swift @@ -129,11 +129,7 @@ public func chatMessageGalleryControllerData( for media in message.media { if let poll = media as? TelegramMediaPoll { standalone = true - if let attachedMedia = poll.attachedMedia as? TelegramMediaMap { - galleryMedia = attachedMedia - } else { - galleryMedia = poll - } + galleryMedia = poll } else if let paidContent = media as? TelegramMediaPaidContent, let extendedMedia = paidContent.extendedMedia.first, case .full = extendedMedia { standalone = true galleryMedia = paidContent @@ -231,6 +227,29 @@ public func chatMessageGalleryControllerData( }, baseNavigationController: navigationController) return .instantPage(gallery, centralIndex, galleryMedia) } else if let galleryMedia = galleryMedia { + var galleryMedia = galleryMedia + if let poll = galleryMedia as? TelegramMediaPoll { + if mediaSubject == nil || mediaSubject == .pollDescription, let attachedMedia = poll.attachedMedia { + if let file = attachedMedia as? TelegramMediaFile, file.isMusic { + galleryMedia = file + } else if let map = attachedMedia as? TelegramMediaMap { + galleryMedia = map + } + } else if case let .pollOption(opaqueIdentifier) = mediaSubject, let optionMedia = poll.options.first(where: { $0.opaqueIdentifier == opaqueIdentifier })?.media { + if let file = optionMedia as? TelegramMediaFile, file.isMusic { + galleryMedia = file + } else if let map = optionMedia as? TelegramMediaMap { + galleryMedia = map + } + } else if case .pollSolution = mediaSubject, let solutionMedia = poll.results.solution?.media { + if let file = solutionMedia as? TelegramMediaFile, file.isMusic { + galleryMedia = file + } else if let map = solutionMedia as? TelegramMediaMap { + galleryMedia = map + } + } + } + if let mapMedia = galleryMedia as? TelegramMediaMap { return .map(mapMedia) } else if let file = galleryMedia as? TelegramMediaFile, (file.isSticker || file.isAnimatedSticker) { diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index ab140949c9..343071ab31 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -436,6 +436,7 @@ public func galleryItemForEntry( context: context, presentationData: presentationData, message: message, + mediaSubject: entry.mediaSubject, location: location, translateToLanguage: translateToLanguage, peerIsCopyProtected: peerIsCopyProtected, @@ -664,8 +665,16 @@ private func galleryEntriesForMessageHistoryEntries(_ entries: [MessageHistoryEn case .pollOption: if let poll = entry.message.media.first(where: { $0 is TelegramMediaPoll }) as? TelegramMediaPoll { for option in poll.options { - if let _ = option.media { - results.append(GalleryEntry(entry: entry, mediaSubject: .pollOption(option.opaqueIdentifier))) + if let optionMedia = option.media { + var isGalleryMedia = false + if optionMedia is TelegramMediaImage { + isGalleryMedia = true + } else if let file = optionMedia as? TelegramMediaFile, file.isVideo || file.mimeType.hasPrefix("image/") { + isGalleryMedia = true + } + if isGalleryMedia { + results.append(GalleryEntry(entry: entry, mediaSubject: .pollOption(option.opaqueIdentifier))) + } } } } diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 007a2f5b50..b5773cc16c 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -3288,7 +3288,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { guard let (message, _, _) = self.contentInfo() else { return false } - var canDelete = false if let peer = message.peers[message.id.peerId] { if peer is TelegramUser || peer is TelegramSecretChat { @@ -3307,6 +3306,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } else { canDelete = false } + if let _ = message.media.first(where: { $0 is TelegramMediaPoll }) { + canDelete = false + } return canDelete } diff --git a/submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent/Sources/MediaPlaybackHeaderPanelComponent.swift b/submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent/Sources/MediaPlaybackHeaderPanelComponent.swift index c8275e6994..3899788a34 100644 --- a/submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent/Sources/MediaPlaybackHeaderPanelComponent.swift +++ b/submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent/Sources/MediaPlaybackHeaderPanelComponent.swift @@ -254,7 +254,8 @@ public final class MediaPlaybackHeaderPanelComponent: Component { } let playerController = component.context.sharedContext.makeOverlayAudioPlayerController(context: controllerContext, chatLocation: .peer(id: id.messageId.peerId), type: component.data.kind, initialMessageId: id.messageId, initialOrder: component.data.playbackOrder, playlistLocation: playlistLocation, parentNavigationController: navigationController) self.window?.endEditing(true) - controller.present(playerController, in: .window(.root)) + playerController.navigationPresentation = .flatModal + controller.push(playerController) case let .messages(chatLocation, _, _): let signal = component.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId)), count: 60, highlight: true, setupReply: false), id: 0), context: component.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic(value: nil), tag: .tag(MessageTags.music)) @@ -295,7 +296,8 @@ public final class MediaPlaybackHeaderPanelComponent: Component { } let playerController = component.context.sharedContext.makeOverlayAudioPlayerController(context: controllerContext, chatLocation: chatLocation, type: component.data.kind, initialMessageId: id.messageId, initialOrder: component.data.playbackOrder, playlistLocation: nil, parentNavigationController: navigationController) self.window?.endEditing(true) - controller.present(playerController, in: .window(.root)) + playerController.navigationPresentation = .flatModal + controller.push(playerController) } else if index.1 { if !progressStarted { progressStarted = true diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPollOptionMedia.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPollOptionMedia.swift index fcb1a9dc31..7989c608ac 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPollOptionMedia.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPollOptionMedia.swift @@ -18,24 +18,18 @@ import StickerPackPreviewUI extension ChatControllerImpl { func openPollMedia(message: Message, subject: ChatControllerInteraction.PollMediaSubject) -> Void { - var media: Media? - var text: String? - var entities: [MessageTextEntity] = [] let mediaSubject: GalleryMediaSubject + var media: Media? switch subject { case let .option(option): - text = option.text - entities = option.entities media = option.media mediaSubject = .pollOption(option.opaqueIdentifier) case let .solution(solution): - text = solution.text - entities = solution.entities media = solution.media mediaSubject = .pollSolution } - guard let _ = text, let media else { + guard let media else { return } @@ -112,13 +106,6 @@ extension ChatControllerImpl { }) } else { - var attributes = message.attributes - attributes.removeAll(where: { $0 is TextEntitiesMessageAttribute }) - if !entities.isEmpty { - attributes.append(TextEntitiesMessageAttribute(entities: entities)) - } - - let message = message //.withUpdatedText(text).withUpdatedAttributes(attributes) let _ = self.context.sharedContext.openChatMessage(OpenChatMessageParams( context: self.context, updatedPresentationData: self.controllerInteraction?.updatedPresentationData, diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 1324d32411..2862519fe9 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -293,7 +293,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu self.historyFrameTopMaskNode = ASImageNode() self.historyFrameTopMaskNode.displaysAsynchronously = false - self.historyFrameTopMaskNode.image = PresentationResourcesItemList.cornersImage(self.presentationData.theme.withModalBlocksBackground(), top: true, bottom: false, glass: true) + self.historyFrameTopMaskNode.image = generateCornersImage(theme: self.presentationData.theme) self.historyFrameTopMaskNode.isUserInteractionEnabled = false let tagMask: MessageTags @@ -633,7 +633,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu self.historyFrameLeftOverlayNode.backgroundColor = self.hasAnyHistoryMessages == true ? self.presentationData.theme.list.modalBlocksBackgroundColor : self.presentationData.theme.list.modalPlainBackgroundColor self.historyFrameRightOverlayNode.backgroundColor = self.hasAnyHistoryMessages == true ? self.presentationData.theme.list.modalBlocksBackgroundColor : self.presentationData.theme.list.modalPlainBackgroundColor self.historyFrameTopOverlayNode.backgroundColor = self.hasAnyHistoryMessages == true ? self.presentationData.theme.list.modalBlocksBackgroundColor : self.presentationData.theme.list.modalPlainBackgroundColor - self.historyFrameTopMaskNode.image = PresentationResourcesItemList.cornersImage(self.presentationData.theme.withModalBlocksBackground(), top: true, bottom: false, glass: true) + self.historyFrameTopMaskNode.image = generateCornersImage(theme: self.presentationData.theme) self.collapseNode.setImage(generateCollapseIcon(theme: self.presentationData.theme), for: []) @@ -1098,7 +1098,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu self.historyFrameLeftOverlayNode.frame = leftOverlayFrame self.historyFrameRightOverlayNode.frame = rightOverlayFrame if let image = self.historyFrameTopMaskNode.image { - self.historyFrameTopMaskNode.frame = CGRect(origin: CGPoint(x: sideInset, y: topOverlayFrame.maxY - UIScreenPixel), size: CGSize(width: layout.size.width - sideInset * 2.0, height: image.size.height)) + self.historyFrameTopMaskNode.frame = CGRect(origin: CGPoint(x: sideInset, y: topOverlayFrame.maxY - 1.0), size: CGSize(width: layout.size.width - sideInset * 2.0, height: image.size.height)) } self.historyFrameTopMaskNode.isHidden = self.controlsNode.hasPlainBackground @@ -1562,3 +1562,22 @@ private func generateCollapseIcon(theme: PresentationTheme) -> UIImage? { context.fillPath() }) } + +private func generateCornersImage(theme: PresentationTheme) -> UIImage? { + return generateImage(CGSize(width: 56.0, height: 56.0), rotatedContext: { (size, context) in + let bounds = CGRect(origin: CGPoint(), size: size) + context.setFillColor(theme.list.blocksBackgroundColor.cgColor) + context.fill(bounds) + + context.setBlendMode(.clear) + + var corners: UIRectCorner = [] + corners.insert(.topLeft) + corners.insert(.topRight) + + let cornerRadius: CGFloat = 26.0 + let path = UIBezierPath(roundedRect: bounds.offsetBy(dx: 0.0, dy: 1.0), byRoundingCorners: corners, cornerRadii: CGSize(width: cornerRadius, height: cornerRadius)) + context.addPath(path.cgPath) + context.fillPath() + })?.stretchableImage(withLeftCapWidth: 28, topCapHeight: 28) +} From c1d8b98fda21b18369b60a17fb0b1f4c9e0b7a0a Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 31 Mar 2026 11:59:27 +0200 Subject: [PATCH 04/50] Update localization --- Telegram/Telegram-iOS/en.lproj/Localizable.strings | 1 + submodules/PremiumUI/Sources/PremiumIntroScreen.swift | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 73d3543c70..9e0743a0fa 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -8771,6 +8771,7 @@ Sorry for the inconvenience."; "Premium.CurrentPlan" = "your current plan"; "Premium.UpgradeFor" = "Upgrade for %@ / month"; "Premium.UpgradeForAnnual" = "Upgrade for %@ / year"; +"Premium.UpgradeForBiannual" = "Upgrade for %@ / 2 years"; "ChatList.PremiumAnnualDiscountTitle" = "Telegram Premium with a discount of %@"; "ChatList.PremiumAnnualDiscountText" = "Sign up for the annual payment plan for Telegram Premium now to get the discount."; diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 3a16ecefbf..7eabf03dfd 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -3792,7 +3792,13 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } else if isUnusedGift { buttonTitle = environment.strings.Premium_Gift_ApplyLink } else if state.isPremium == true && state.canUpgrade { - buttonTitle = state.isAnnual ? environment.strings.Premium_UpgradeForAnnual(state.price ?? "—").string : environment.strings.Premium_UpgradeFor(state.price ?? "—").string + if state.isAnnual { + buttonTitle = environment.strings.Premium_UpgradeForAnnual(state.price ?? "—").string + } else if state.isBiannual { + buttonTitle = environment.strings.Premium_UpgradeForBiannual(state.price ?? "—").string + } else { + buttonTitle = environment.strings.Premium_UpgradeFor(state.price ?? "—").string + } } else { if state.isAnnual { buttonTitle = environment.strings.Premium_SubscribeForAnnual(state.price ?? "—").string @@ -3803,9 +3809,6 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } } - - - let controller = environment.controller let button = button.update( component: SolidRoundedButtonComponent( From 36440d65663f2cc35cf38f9cbebe96dbec8eb39c Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 31 Mar 2026 13:51:10 +0200 Subject: [PATCH 05/50] Album art view --- .../Sources/MediaPickerScreen.swift | 2 +- .../Sources/PhotoResources.swift | 55 +++++++-- .../OverlayAudioPlayerControllerNode.swift | 79 ++++++++++++- .../OverlayAudioPlayerControlsNode.swift | 109 ++---------------- 4 files changed, 131 insertions(+), 114 deletions(-) diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 6de88cee9e..c7b6ffae17 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -441,7 +441,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att if case let .assets(_, mode) = controller.subject { switch mode { - case .default, .story, .poll: + case .default, .poll: self.containerNode.view.addSubview(self.bottomEdgeEffectView) default: break diff --git a/submodules/PhotoResources/Sources/PhotoResources.swift b/submodules/PhotoResources/Sources/PhotoResources.swift index 0d2a373bb0..5d66b24aa6 100644 --- a/submodules/PhotoResources/Sources/PhotoResources.swift +++ b/submodules/PhotoResources/Sources/PhotoResources.swift @@ -3162,7 +3162,6 @@ private func albumArtFullSizeDatas(engine: TelegramEngine, file: FileMediaRefere return .single(Tuple(nil, nil, false)) } } - } } |> distinctUntilChanged(isEqual: { lhs, rhs in @@ -3229,25 +3228,32 @@ public func playerAlbumArt(postbox: Postbox, engine: TelegramEngine, fileReferen } ) } - - var immediateArtworkData: Signal, NoError> = .single(Tuple(nil, nil, false)) - - if let fileReference = fileReference, let smallestRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { + + func previewArtworkData(fileReference: FileMediaReference) -> Signal { + guard let smallestRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) else { + return .single(nil) + } + let thumbnailResource = smallestRepresentation.resource - let fetchedThumbnail = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: .other, userContentType: .image, reference: fileReference.resourceReference(thumbnailResource)) - - let thumbnail = Signal { subscriber in + + return Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() let thumbnailDisposable = postbox.mediaBox.resourceData(thumbnailResource, attemptSynchronously: attemptSynchronously).start(next: { next in subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) }, error: subscriber.putError, completed: subscriber.putCompletion) - + return ActionDisposable { fetchedDisposable.dispose() thumbnailDisposable.dispose() } } + } + + var immediateArtworkData: Signal, NoError> = .single(Tuple(nil, nil, false)) + + if let fileReference = fileReference, thumbnail, smallestImageRepresentation(fileReference.media.previewRepresentations) != nil { + let thumbnail = previewArtworkData(fileReference: fileReference) immediateArtworkData = thumbnail |> map { thumbnailData in return Tuple(thumbnailData, nil, false) @@ -3259,7 +3265,36 @@ public func playerAlbumArt(postbox: Postbox, engine: TelegramEngine, fileReferen return Tuple(thumbnailData, nil, false) } } else { - immediateArtworkData = albumArtFullSizeDatas(engine: engine, file: fileReference, thumbnail: albumArt.thumbnailResource, fullSize: albumArt.fullSizeResource) + let previewData: Signal + if let fileReference = fileReference { + previewData = previewArtworkData(fileReference: fileReference) + } else { + previewData = .single(nil) + } + + immediateArtworkData = combineLatest( + albumArtFullSizeDatas(engine: engine, file: fileReference, thumbnail: albumArt.thumbnailResource, fullSize: albumArt.fullSizeResource), + previewData + ) + |> map { remoteArtworkData, previewData in + let remoteFullSizeData = remoteArtworkData._1 + let shouldUsePreviewFallback: Bool + if remoteArtworkData._2 { + if let remoteFullSizeData = remoteFullSizeData { + shouldUsePreviewFallback = remoteFullSizeData.isEmpty + } else { + shouldUsePreviewFallback = true + } + } else { + shouldUsePreviewFallback = false + } + + if shouldUsePreviewFallback { + return Tuple(previewData, nil, false) + } else { + return remoteArtworkData + } + } } } else { immediateArtworkData = .single(Tuple(nil, nil, false)) diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 2862519fe9..97d8168aaf 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -17,6 +17,7 @@ import UndoUI import ChatHistoryEntry import MultilineTextComponent import GlassControls +import PhotoResources final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestureRecognizerDelegate { let ready = Promise() @@ -61,6 +62,10 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu private var replacementHistoryNode: ChatHistoryListNodeImpl? private var replacementHistoryNodeFloatingOffset: CGFloat? + private var currentAlbumArt: (FileMediaReference, SharedMediaPlaybackAlbumArt)? + private let albumArtBackground: UIVisualEffectView + private let albumArtNode = TransformImageNode() + private var saveMediaDisposable: MetaDisposable? private var validLayout: ContainerViewLayout? @@ -335,6 +340,10 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu self.collapseNode.displaysAsynchronously = false self.collapseNode.setImage(generateCollapseIcon(theme: self.presentationData.theme), for: []) + self.albumArtBackground = UIVisualEffectView() + self.albumArtBackground.contentView.addSubview(self.albumArtNode.view) + self.albumArtNode.isUserInteractionEnabled = false + super.init() self.backgroundColor = nil @@ -376,6 +385,19 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu } } + self.controlsNode.requestAlbumArtDisplay = { [weak self] fileReferenceAndAlbumArt in + guard let self, let layout = self.validLayout else { + return + } + self.currentAlbumArt = fileReferenceAndAlbumArt + + if let (fileReference, albumArt) = fileReferenceAndAlbumArt { + self.albumArtNode.setSignal(playerAlbumArt(postbox: self.context.account.postbox, engine: self.context.engine, fileReference: fileReference, albumArt: albumArt, thumbnail: false)) + } + + self.containerLayoutUpdated(layout, transition: .animated(duration: 0.25, curve: .easeInOut)) + } + self.controlsNode.requestCollapse = { [weak self] in self?.requestDismiss() } @@ -542,6 +564,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu ) self.setupReordering() + + self.albumArtBackground.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.albumArtTapped(_:)))) } deinit { @@ -841,7 +865,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu let controlsHeight = self.controlsNode.updateLayout(width: layout.size.width, leftInset: 0.0, rightInset: 0.0, bottomInset: layout.intrinsicInsets.bottom, maxHeight: layout.size.height, savedMusic: self.isSaved, transition: transition) let controlsFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - controlsHeight), size: CGSize(width: layout.size.width, height: controlsHeight)) - transition.updateFrame(node: self.controlsNode, frame: controlsFrame) + let controlsTransition = self.controlsNode.frame.width > 0.0 ? transition : .immediate + controlsTransition.updateFrame(node: self.controlsNode, frame: controlsFrame) let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top) var insets = UIEdgeInsets() @@ -962,6 +987,49 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu self.updateHistoryContentOffset(self.historyNode.visibleContentOffset(), transition: transition) + self.albumArtBackground.frame = CGRect(origin: .zero, size: layout.size) + + if let _ = self.currentAlbumArt { + var animateIn = false + if self.albumArtBackground.superview == nil { + self.view.addSubview(self.albumArtBackground) + animateIn = true + } + let albumArtSide = min(360.0, layout.size.width - 32.0) + let albumArtSize = CGSize(width: albumArtSide, height: albumArtSide) + self.albumArtNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - albumArtSize.width) / 2.0), y: floorToScreenPixels((layout.size.height - albumArtSize.height) / 2.0)), size: albumArtSize) + + let makeLargeAlbumArtLayout = self.albumArtNode.asyncLayout() + let applyLargeAlbumArt = makeLargeAlbumArtLayout(TransformImageArguments(corners: ImageCorners(radius: 4.0), imageSize: albumArtSize, boundingSize: albumArtSize, intrinsicInsets: UIEdgeInsets())) + applyLargeAlbumArt() + + if animateIn { + self.controlsNode.albumArtNode.alpha = 0.0 + + let sourceFrame = self.controlsNode.albumArtNode.view.convert(self.controlsNode.albumArtNode.bounds, to: self.albumArtBackground.contentView) + ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring).animateFrame(node: self.albumArtNode, from: sourceFrame) + UIView.animate(withDuration: 0.2, animations: { + self.albumArtBackground.effect = UIBlurEffect(style: self.presentationData.theme.overallDarkAppearance ? .dark : .light) + }) + } + } else { + self.controlsNode.albumArtNode.alpha = 1.0 + + if self.albumArtBackground.superview != nil { + let fadeTransition = ComponentTransition(transition) + fadeTransition.setBlur(layer: self.albumArtNode.layer, radius: 10.0) + fadeTransition.setAlpha(layer: self.albumArtNode.layer, alpha: 0.0) + + UIView.animate(withDuration: 0.2, animations: { + self.albumArtBackground.effect = nil + }, completion: { _ in + self.albumArtBackground.removeFromSuperview() + ComponentTransition.immediate.setBlur(layer: self.albumArtNode.layer, radius: 0.0) + ComponentTransition.immediate.setAlpha(layer: self.albumArtNode.layer, alpha: 1.0) + }) + } + } + var layout = layout layout.intrinsicInsets.bottom = controlsHeight + (self.historyNode.hasAnyMessages ? 0.0 : 8.0) self.getParentController()?.presentationContext.containerLayoutUpdated(layout, transition: transition) @@ -999,6 +1067,13 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu self.requestDismiss() } } + + @objc func albumArtTapped(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state, let layout = self.validLayout { + self.currentAlbumArt = nil + self.containerLayoutUpdated(layout, transition: .animated(duration: 0.3, curve: .easeInOut)) + } + } override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { if let recognizer = gestureRecognizer as? UIPanGestureRecognizer { @@ -1566,7 +1641,7 @@ private func generateCollapseIcon(theme: PresentationTheme) -> UIImage? { private func generateCornersImage(theme: PresentationTheme) -> UIImage? { return generateImage(CGSize(width: 56.0, height: 56.0), rotatedContext: { (size, context) in let bounds = CGRect(origin: CGPoint(), size: size) - context.setFillColor(theme.list.blocksBackgroundColor.cgColor) + context.setFillColor(theme.list.modalBlocksBackgroundColor.cgColor) context.fill(bounds) context.setBlendMode(.clear) diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControlsNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControlsNode.swift index 546ddc9884..295df935ca 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControlsNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControlsNode.swift @@ -148,8 +148,7 @@ final class OverlayAudioPlayerControlsNode: ASDisplayNode { private let backgroundNode: ASImageNode - private let albumArtNode: TransformImageNode - private var largeAlbumArtNode: TransformImageNode? + let albumArtNode: TransformImageNode private let titleNode: TextNode private let title: ComponentView private let descriptionNode: TextNode @@ -193,6 +192,7 @@ final class OverlayAudioPlayerControlsNode: ASDisplayNode { var isExpanded = false var updateIsExpanded: (() -> Void)? + var requestAlbumArtDisplay: (((FileMediaReference, SharedMediaPlaybackAlbumArt)?) -> Void)? var requestCollapse: (() -> Void)? var requestShare: ((ShareControllerSubject) -> Void)? @@ -222,7 +222,6 @@ final class OverlayAudioPlayerControlsNode: ASDisplayNode { private let hapticFeedback = HapticFeedback() - private var scrubbingDisposable: Disposable? private var leftDurationLabelPushed = false private var rightDurationLabelPushed = false private var infoNodePushed = false @@ -356,33 +355,7 @@ final class OverlayAudioPlayerControlsNode: ASDisplayNode { self.scrubberNode.status = mappedStatus self.leftDurationLabel.status = mappedStatus self.rightDurationLabel.status = mappedStatus - -// self.scrubbingDisposable = (self.scrubberNode.scrubbingPosition -// |> deliverOnMainQueue).startStrict(next: { [weak self] value in -// guard let strongSelf = self else { -// return -// } -// let leftDurationLabelPushed: Bool -// let rightDurationLabelPushed: Bool -// let infoNodePushed: Bool -// if let value = value { -// leftDurationLabelPushed = value < 0.16 -// rightDurationLabelPushed = value > (strongSelf.rateButton.isHidden ? 0.84 : 0.74) -// infoNodePushed = value >= 0.16 && value <= 0.84 -// } else { -// leftDurationLabelPushed = false -// rightDurationLabelPushed = false -// infoNodePushed = false -// } -// if leftDurationLabelPushed != strongSelf.leftDurationLabelPushed || rightDurationLabelPushed != strongSelf.rightDurationLabelPushed || infoNodePushed != strongSelf.infoNodePushed { -// strongSelf.leftDurationLabelPushed = leftDurationLabelPushed -// strongSelf.rightDurationLabelPushed = rightDurationLabelPushed -// strongSelf.infoNodePushed = infoNodePushed -// -// strongSelf.requestLayout(transition: .animated(duration: 0.35, curve: .spring)) -// } -// }) - + self.statusDisposable = combineLatest( queue: Queue.mainQueue(), delayedStatus, @@ -592,7 +565,6 @@ final class OverlayAudioPlayerControlsNode: ASDisplayNode { deinit { self.statusDisposable?.dispose() self.chapterDisposable?.dispose() - self.scrubbingDisposable?.dispose() } override func didLoad() { @@ -788,9 +760,7 @@ final class OverlayAudioPlayerControlsNode: ASDisplayNode { self.currentAlbumArtInitialized = true self.currentAlbumArt = albumArt self.albumArtNode.setSignal(playerAlbumArt(postbox: self.account.postbox, engine: self.engine, fileReference: self.currentFileReference, albumArt: albumArt, thumbnail: true)) - if let largeAlbumArtNode = self.largeAlbumArtNode { - largeAlbumArtNode.setSignal(playerAlbumArt(postbox: self.account.postbox, engine: self.engine, fileReference: self.currentFileReference, albumArt: albumArt, thumbnail: false)) - } + self.requestAlbumArtDisplay?(nil) } } @@ -880,72 +850,8 @@ final class OverlayAudioPlayerControlsNode: ASDisplayNode { let applyAlbumArt = makeAlbumArtLayout(TransformImageArguments(corners: ImageCorners(radius: 10.0), imageSize: albumArtSize, boundingSize: albumArtSize, intrinsicInsets: UIEdgeInsets())) applyAlbumArt() let albumArtFrame = CGRect(origin: CGPoint(x: leftInset + sideInset, y: infoVerticalOrigin - 3.0), size: albumArtSize) - let previousAlbumArtNodeFrame = self.albumArtNode.frame transition.updateFrame(node: self.albumArtNode, frame: albumArtFrame) - if self.isExpanded { - let largeAlbumArtNode: TransformImageNode - var animateIn = false - if let current = self.largeAlbumArtNode { - largeAlbumArtNode = current - } else { - animateIn = true - largeAlbumArtNode = TransformImageNode() - self.largeAlbumArtNode = largeAlbumArtNode - self.addSubnode(largeAlbumArtNode) - if self.currentAlbumArtInitialized { - largeAlbumArtNode.setSignal(playerAlbumArt(postbox: self.account.postbox, engine: self.engine, fileReference: self.currentFileReference, albumArt: self.currentAlbumArt, thumbnail: false)) - } - } - - let albumArtHeight = max(1.0, panelHeight - OverlayAudioPlayerControlsNode.basePanelHeight - 24.0) - - let largeAlbumArtSize = CGSize(width: albumArtHeight, height: albumArtHeight) - let makeLargeAlbumArtLayout = largeAlbumArtNode.asyncLayout() - let applyLargeAlbumArt = makeLargeAlbumArtLayout(TransformImageArguments(corners: ImageCorners(radius: 4.0), imageSize: largeAlbumArtSize, boundingSize: largeAlbumArtSize, intrinsicInsets: UIEdgeInsets())) - applyLargeAlbumArt() - - let largeAlbumArtFrame = CGRect(origin: CGPoint(x: floor((width - largeAlbumArtSize.width) / 2.0), y: 34.0), size: largeAlbumArtSize) - - if animateIn && transition.isAnimated { - largeAlbumArtNode.frame = largeAlbumArtFrame - transition.animatePositionAdditive(node: largeAlbumArtNode, offset: CGPoint(x: previousAlbumArtNodeFrame.center.x - largeAlbumArtFrame.center.x, y: previousAlbumArtNodeFrame.center.y - largeAlbumArtFrame.center.y)) - //largeAlbumArtNode.layer.animatePosition(from: CGPoint(x: -50.0, y: 0.0), to: CGPoint(), duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, additive: true) - transition.animateTransformScale(node: largeAlbumArtNode, from: previousAlbumArtNodeFrame.size.height / largeAlbumArtFrame.size.height) - largeAlbumArtNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12) - if let copyView = self.albumArtNode.view.snapshotContentTree() { - copyView.frame = previousAlbumArtNodeFrame - copyView.center = largeAlbumArtFrame.center - self.view.insertSubview(copyView, belowSubview: largeAlbumArtNode.view) - transition.animatePositionAdditive(layer: copyView.layer, offset: CGPoint(x: previousAlbumArtNodeFrame.center.x - largeAlbumArtFrame.center.x, y: previousAlbumArtNodeFrame.center.y - largeAlbumArtFrame.center.y), completion: { [weak copyView] _ in - copyView?.removeFromSuperview() - }) - //copyView.layer.animatePosition(from: CGPoint(x: -50.0, y: 0.0), to: CGPoint(), duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, additive: true) - copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.28, removeOnCompletion: false) - transition.updateTransformScale(layer: copyView.layer, scale: largeAlbumArtFrame.size.height / previousAlbumArtNodeFrame.size.height) - } - } else { - transition.updateFrame(node: largeAlbumArtNode, frame: largeAlbumArtFrame) - } - self.albumArtNode.isHidden = true - } else if let largeAlbumArtNode = self.largeAlbumArtNode { - self.largeAlbumArtNode = nil - self.albumArtNode.isHidden = false - if transition.isAnimated { - transition.animatePosition(node: self.albumArtNode, from: largeAlbumArtNode.frame.center) - transition.animateTransformScale(node: self.albumArtNode, from: largeAlbumArtNode.frame.height / self.albumArtNode.frame.height) - self.albumArtNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12) - - transition.updatePosition(node: largeAlbumArtNode, position: self.albumArtNode.frame.center, completion: { [weak largeAlbumArtNode] _ in - largeAlbumArtNode?.removeFromSupernode() - }) - largeAlbumArtNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.28, removeOnCompletion: false) - transition.updateTransformScale(node: largeAlbumArtNode, scale: self.albumArtNode.frame.height / largeAlbumArtNode.frame.height) - } else { - largeAlbumArtNode.removeFromSupernode() - } - } - let scrubberVerticalOrigin: CGFloat = infoVerticalOrigin + 58.0 let scrubberInset: CGFloat = 9.0 @@ -1283,15 +1189,16 @@ final class OverlayAudioPlayerControlsNode: ASDisplayNode { } @objc func albumArtTap(_ recognizer: UITapGestureRecognizer) { - if !"".isEmpty, case .ended = recognizer.state { + if case .ended = recognizer.state { if let supernode = self.supernode { let bounds = supernode.bounds if bounds.width > bounds.height { return } } - self.isExpanded = !self.isExpanded - self.updateIsExpanded?() + if let currentFileReference = self.currentFileReference, let currentAlbumArt = self.currentAlbumArt { + self.requestAlbumArtDisplay?((currentFileReference, currentAlbumArt)) + } } } From 34477605b1be438ba0f6423c398ff31079a0cfd3 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 31 Mar 2026 20:24:24 +0800 Subject: [PATCH 06/50] Various improvements --- Telegram/SiriIntents/IntentHandler.swift | 4 +- .../AuthorizationSequenceController.swift | 2 +- .../Sources/ChatListControllerNode.swift | 16 +- .../Sources/ChatListSearchContainerNode.swift | 26 +- .../Sources/ChatListSearchListPaneNode.swift | 52 ++- .../Sources/PagerComponent.swift | 14 + submodules/Display/Source/NavigationBar.swift | 10 +- .../GalleryUI/Sources/GalleryController.swift | 2 +- .../HashtagSearchGlobalChatContents.swift | 4 +- .../SetupTwoStepVerificationController.swift | 2 +- .../TwoFactorAuthDataInputScreen.swift | 2 +- .../Sources/TwoFactorAuthSplashScreen.swift | 2 +- submodules/Postbox/Sources/Postbox.swift | 38 +- .../QrCodeUI/Sources/QrCodeScanScreen.swift | 2 +- .../SearchBarNode/Sources/SearchBarNode.swift | 3 + .../Contacts/TelegramEngineContacts.swift | 4 +- .../Messages/SearchMessages.swift | 12 +- .../TelegramNotices/Sources/Notices.swift | 32 ++ .../Sources/ComponentsThemes.swift | 2 +- .../Sources/AttachmentFileController.swift | 2 +- .../Sources/AttachmentFileSearchItem.swift | 2 +- .../Sources/ChatEntityKeyboardInputNode.swift | 35 +- .../Sources/ChatScheduleTimeScreen.swift | 22 +- .../Sources/EntityKeyboard.swift | 27 +- .../Sources/NavigationBarImpl.swift | 69 +++- .../Sources/NavigationButtonNode.swift | 85 +++-- .../Sources/PeerInfoScreen.swift | 6 +- .../Sources/PeerSelectionControllerNode.swift | 1 + .../Chat/ChatControllerLoadDisplayNode.swift | 4 +- .../TelegramUI/Sources/ChatController.swift | 2 +- .../Sources/ChatControllerContentData.swift | 33 +- .../ChatControllerOpenCalendarSearch.swift | 335 ++++++++++-------- .../ChatInterfaceStateNavigationButtons.swift | 10 +- .../Sources/WebSearchGalleryController.swift | 2 +- .../WebUI/Sources/WebAppController.swift | 4 +- 35 files changed, 600 insertions(+), 268 deletions(-) diff --git a/Telegram/SiriIntents/IntentHandler.swift b/Telegram/SiriIntents/IntentHandler.swift index 46395e888b..b0381122af 100644 --- a/Telegram/SiriIntents/IntentHandler.swift +++ b/Telegram/SiriIntents/IntentHandler.swift @@ -815,7 +815,7 @@ class DefaultIntentHandler: INExtension, INSendMessageIntentHandling, INSearchFo if let searchTerm = searchTerm { if !searchTerm.isEmpty { - for renderedPeer in transaction.searchPeers(query: searchTerm) { + for renderedPeer in transaction.searchPeers(query: searchTerm, predicate: nil) { if let peer = renderedPeer.peer, !(peer is TelegramSecretChat), !peer.isDeleted { peers.append(peer) } @@ -988,7 +988,7 @@ private final class WidgetIntentHandler { if let searchTerm = searchTerm { if !searchTerm.isEmpty { - for renderedPeer in transaction.searchPeers(query: searchTerm) { + for renderedPeer in transaction.searchPeers(query: searchTerm, predicate: nil) { if let peer = renderedPeer.peer, !(peer is TelegramSecretChat), !peer.isDeleted { peers.append(peer) } diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift index bc640f69bf..70780df037 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift @@ -34,7 +34,7 @@ private enum InnerState: Equatable { public final class AuthorizationSequenceController: NavigationController, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { static func navigationBarTheme(_ theme: PresentationTheme) -> NavigationBarTheme { - return NavigationBarTheme(overallDarkAppearance: theme.overallDarkAppearance, buttonColor: theme.chat.inputPanel.panelControlColor, disabledButtonColor: theme.intro.disabledTextColor, primaryTextColor: theme.intro.primaryTextColor, backgroundColor: .clear, opaqueBackgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: theme.rootController.navigationBar.badgeBackgroundColor, badgeStrokeColor: theme.rootController.navigationBar.badgeStrokeColor, badgeTextColor: theme.rootController.navigationBar.badgeTextColor, edgeEffectColor: .clear, style: .glass) + return NavigationBarTheme(overallDarkAppearance: theme.overallDarkAppearance, buttonColor: theme.chat.inputPanel.panelControlColor, disabledButtonColor: theme.intro.disabledTextColor, primaryTextColor: theme.intro.primaryTextColor, backgroundColor: .clear, opaqueBackgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: theme.rootController.navigationBar.badgeBackgroundColor, badgeStrokeColor: theme.rootController.navigationBar.badgeStrokeColor, badgeTextColor: theme.rootController.navigationBar.badgeTextColor, edgeEffectColor: .clear, accentButtonColor: theme.list.itemCheckColors.fillColor, accentForegroundColor: theme.list.itemCheckColors.foregroundColor, style: .glass) } private let sharedContext: SharedAccountContext diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 91c9505a72..8281e29585 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1949,7 +1949,21 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { //filter.insert(.excludeRecent) } - let contentNode = ChatListSearchContainerNode(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, filter: filter, requestPeerType: nil, location: effectiveLocation, displaySearchFilters: displaySearchFilters, hasDownloads: hasDownloads, initialFilter: initialFilter, openPeer: { [weak self] peer, _, threadId, dismissSearch in + var folder: (Int32, String)? + if let folders = self.controller?.tabContainerData?.0 { + switch self.effectiveContainerNode.currentItemFilter { + case .all: + break + case let .filter(id): + if let value = folders.first(where: { $0.id == .filter(id) }) { + if case let .filter(_, text, _) = value { + folder = (id, text.text) + } + } + } + } + + let contentNode = ChatListSearchContainerNode(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, filter: filter, requestPeerType: nil, location: effectiveLocation, folder: folder, displaySearchFilters: displaySearchFilters, hasDownloads: hasDownloads, initialFilter: initialFilter, openPeer: { [weak self] peer, _, threadId, dismissSearch in self?.requestOpenPeerFromSearch?(peer, threadId, dismissSearch) }, openDisabledPeer: { _, _, _ in }, openRecentPeerOptions: { [weak self] peer in diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 2495e589ee..b736750998 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -43,6 +43,7 @@ import ComponentDisplayAdapters private enum ChatListTokenId: Int32 { case archive + case folder case forum case filter case peer @@ -102,6 +103,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo private let peersFilter: ChatListNodePeersFilter private let requestPeerType: [ReplyMarkupButtonRequestPeerType]? private var location: ChatListControllerLocation + private var folder: (Int32, String)? private let displaySearchFilters: Bool private let hasDownloads: Bool private var interaction: ChatListSearchInteraction? @@ -165,7 +167,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo private var recentAppsDisposable: Disposable? private var refreshedGlobalPostSearchStateDisposable: Disposable? - public init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, filter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter = .chats, openPeer originalOpenPeer: @escaping (EnginePeer, EnginePeer?, Int64?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void, openRecentPeerOptions: @escaping (EnginePeer) -> Void, openMessage originalOpenMessage: @escaping (EnginePeer, Int64?, EngineMessage.Id, Bool) -> Void, addContact: ((String) -> Void)?, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, navigationController: NavigationController?, parentController: @escaping () -> ViewController?) { + public init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, filter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, folder: (Int32, String)?, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter = .chats, openPeer originalOpenPeer: @escaping (EnginePeer, EnginePeer?, Int64?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void, openRecentPeerOptions: @escaping (EnginePeer) -> Void, openMessage originalOpenMessage: @escaping (EnginePeer, Int64?, EngineMessage.Id, Bool) -> Void, addContact: ((String) -> Void)?, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, navigationController: NavigationController?, parentController: @escaping () -> ViewController?) { var initialFilter = initialFilter if case .chats = initialFilter, case .forum = location { initialFilter = .topics @@ -175,6 +177,9 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.peersFilter = filter self.requestPeerType = requestPeerType self.location = location + + self.folder = folder + self.displaySearchFilters = displaySearchFilters self.hasDownloads = hasDownloads self.navigationController = navigationController @@ -194,6 +199,11 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.paneContainerNode.clipsToBounds = true super.init() + + if let folder { + self.searchOptionsValue = self.currentSearchOptions.withUpdatedFolder(folder) + self.searchOptions.set(.single(self.currentSearchOptions)) + } self.backgroundColor = filter.contains(.excludeRecent) ? nil : self.presentationData.theme.chatList.backgroundColor @@ -588,7 +598,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } private var currentSearchOptions: ChatListSearchOptions { - return self.searchOptionsValue ?? ChatListSearchOptions(peer: nil, date: nil) + return self.searchOptionsValue ?? ChatListSearchOptions(peer: nil, date: nil, folder: nil) } public override func searchTokensUpdated(tokens: [SearchBarToken]) { @@ -609,12 +619,19 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo if !tokensIdSet.contains(ChatListTokenId.peer.rawValue) && updatedOptions?.peer != nil { updatedOptions = updatedOptions?.withUpdatedPeer(nil) } + if !tokensIdSet.contains(ChatListTokenId.folder.rawValue) && updatedOptions?.folder != nil { + updatedOptions = updatedOptions?.withUpdatedFolder(nil) + self.folder = nil + } self.updateSearchOptions(updatedOptions) } private func updateSearchOptions(_ options: ChatListSearchOptions?, clearQuery: Bool = false) { var options = options var tokens: [SearchBarToken] = [] + if let folder = self.folder { + tokens.append(SearchBarToken(id: ChatListTokenId.folder.rawValue, icon: nil, iconOffset: 0.0, peer: nil, title: folder.1, permanent: false)) + } if case .chatList(.archive) = self.location { tokens.append(SearchBarToken(id: ChatListTokenId.archive.rawValue, icon: UIImage(bundleImageName: "Chat List/Search/Archive"), iconOffset: -1.0, title: self.presentationData.strings.ChatList_Archive, permanent: false)) } else if case .forum = self.location, let forumPeer = self.forumPeer { @@ -737,10 +754,13 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo key = .chats } self.paneContainerNode.requestSelectPane(key) - self.updateSearchOptions(nil) + self.updateSearchOptions(self.currentSearchOptions) self.searchTextUpdated(text: query ?? "") var tokens: [SearchBarToken] = [] + if let folder = self.folder { + tokens.append(SearchBarToken(id: ChatListTokenId.folder.rawValue, icon: nil, iconOffset: 0.0, peer: nil, title: folder.1, permanent: false)) + } if case .chatList(.archive) = self.location { tokens.append(SearchBarToken(id: ChatListTokenId.archive.rawValue, icon: UIImage(bundleImageName: "Chat List/Search/Archive"), iconOffset: -1.0, title: self.presentationData.strings.ChatList_Archive, permanent: false)) } else if case .forum = self.location, let forumPeer = self.forumPeer { diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 7329c1f7ed..52b8ba2e16 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -1448,17 +1448,22 @@ public enum ChatListSearchContextActionSource { public struct ChatListSearchOptions { let peer: (EnginePeer.Id, Bool, String)? let date: (Int32?, Int32, String)? + let folder: (Int32, String)? var isEmpty: Bool { - return self.peer == nil && self.date == nil + return self.peer == nil && self.date == nil && self.folder == nil } func withUpdatedPeer(_ peerIdIsGroupAndName: (EnginePeer.Id, Bool, String)?) -> ChatListSearchOptions { - return ChatListSearchOptions(peer: peerIdIsGroupAndName, date: self.date) + return ChatListSearchOptions(peer: peerIdIsGroupAndName, date: self.date, folder: self.folder) } func withUpdatedDate(_ minDateMaxDateAndTitle: (Int32?, Int32, String)?) -> ChatListSearchOptions { - return ChatListSearchOptions(peer: self.peer, date: minDateMaxDateAndTitle) + return ChatListSearchOptions(peer: self.peer, date: minDateMaxDateAndTitle, folder: self.folder) + } + + func withUpdatedFolder(_ folder: (Int32, String)?) -> ChatListSearchOptions { + return ChatListSearchOptions(peer: self.peer, date: self.date, folder: folder) } } @@ -2065,7 +2070,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { var defaultFoundRemoteMessagesSignal: Signal<([FoundRemoteMessages], Bool), NoError> = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) if key == .globalPosts, let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["ios_load_empty_global_posts"] as? Double, value != 0.0 { - let searchSignal = context.engine.messages.searchMessages(location: .general(scope: .globalPosts(allowPaidStars: nil), 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, folderId: nil), query: "", state: nil, limit: 50) |> map { resultData -> ChatListSearchMessagesResult in let (result, updatedState) = resultData @@ -2089,7 +2094,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let foundItems: Signal<([ChatListSearchEntry], Bool, String?)?, NoError> = combineLatest(queue: .mainQueue(), searchQuery, self.approvedGlobalPostQueryState.get(), searchOptions, self.searchScopePromise.get(), downloadItems, globalPostSearchStateType, isPremium) |> 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) { + if query == nil && (options == nil || options?.withUpdatedFolder(nil).isEmpty == true) && [.chats, .topics, .channels, .apps].contains(key) { let _ = currentRemotePeers.swap(nil) return .single(nil) } @@ -2244,7 +2249,24 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { return result } - let updatedLocalPeers = context.engine.contacts.searchLocalPeers(query: query.lowercased()) + var predicate: Signal = .single(nil) + if let folderId = options?.folder?.0 { + predicate = context.engine.peers.currentChatListFilters() + |> take(1) + |> map { filters -> ChatListFilterPredicate? in + guard let filter = filters.first(where: { $0.id == folderId }) else { + return nil + } + guard case let .filter(_, _, _, data) = filter else { + return nil + } + return chatListFilterPredicate(filter: data, accountPeerId: context.account.peerId) + } + } + + let updatedLocalPeers = predicate |> mapToSignal { predicate in + return context.engine.contacts.searchLocalPeers(query: query.lowercased(), predicate: predicate) + } |> mapToSignal { peers -> Signal<[EngineRenderedPeer], NoError> in return context.engine.data.subscribe( EngineDataMap(peers.map { peer in @@ -2552,7 +2574,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if case .savedMessagesChats = location { foundRemotePeers = .single(([], [], [], false)) } else if let query = query, case .chats = key { - if query.hasPrefix("#") { + if query.hasPrefix("#") || options?.folder != nil { foundRemotePeers = .single(([], [], [], false)) } else { foundRemotePeers = ( @@ -2584,28 +2606,28 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } let searchLocations: [SearchMessagesLocation] if key == .globalPosts { - searchLocations = [SearchMessagesLocation.general(scope: .globalPosts(allowPaidStars: approvedGlobalPostQueryState?.price), tags: nil, minDate: nil, maxDate: nil)] - } else if let options = options { + searchLocations = [SearchMessagesLocation.general(scope: .globalPosts(allowPaidStars: approvedGlobalPostQueryState?.price), tags: nil, minDate: nil, maxDate: nil, folderId: nil)] + } else if let options { if case let .forum(peerId) = location { - searchLocations = [.peer(peerId: peerId, fromId: nil, tags: tagMask, reactions: nil, threadId: nil, minDate: options.date?.0, maxDate: options.date?.1), .general(scope: .everywhere, tags: tagMask, minDate: options.date?.0, maxDate: options.date?.1)] + searchLocations = [.peer(peerId: peerId, fromId: nil, tags: tagMask, reactions: nil, threadId: nil, minDate: options.date?.0, maxDate: options.date?.1), .general(scope: .everywhere, tags: tagMask, minDate: options.date?.0, maxDate: options.date?.1, folderId: nil)] } else if let (peerId, _, _) = options.peer { searchLocations = [.peer(peerId: peerId, fromId: nil, tags: tagMask, reactions: nil, threadId: nil, minDate: options.date?.0, maxDate: options.date?.1)] } else { if case let .chatList(groupId) = location, case .archive = groupId { searchLocations = [.group(groupId: groupId._asGroup(), tags: tagMask, minDate: options.date?.0, maxDate: options.date?.1)] } else { - searchLocations = [.general(scope: searchScope, tags: tagMask, minDate: options.date?.0, maxDate: options.date?.1)] + searchLocations = [.general(scope: searchScope, tags: tagMask, minDate: options.date?.0, maxDate: options.date?.1, folderId: options.folder?.0)] } } } else { if case .channels = key { - searchLocations = [.general(scope: .channels, tags: tagMask, minDate: nil, maxDate: nil)] + searchLocations = [.general(scope: .channels, tags: tagMask, minDate: nil, maxDate: nil, folderId: nil)] } else if case let .forum(peerId) = location { - searchLocations = [.peer(peerId: peerId, fromId: nil, tags: tagMask, reactions: nil, threadId: nil, minDate: nil, maxDate: nil), .general(scope: .everywhere, tags: tagMask, minDate: nil, maxDate: nil)] + searchLocations = [.peer(peerId: peerId, fromId: nil, tags: tagMask, reactions: nil, threadId: nil, minDate: nil, maxDate: nil), .general(scope: .everywhere, tags: tagMask, minDate: nil, maxDate: nil, folderId: nil)] } else if case let .chatList(groupId) = location, case .archive = groupId { searchLocations = [.group(groupId: groupId._asGroup(), tags: tagMask, minDate: nil, maxDate: nil)] } else { - searchLocations = [.general(scope: searchScope, tags: tagMask, minDate: nil, maxDate: nil)] + searchLocations = [.general(scope: searchScope, tags: tagMask, minDate: nil, maxDate: nil, folderId: nil)] } } @@ -2704,7 +2726,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let searchSignals: [Signal<(SearchMessagesResult, SearchMessagesState), NoError>] if key == .globalPosts { - searchSignals = [context.engine.messages.searchMessages(location: .general(scope: .globalPosts(allowPaidStars: approvedGlobalPostQueryState?.price), tags: nil, minDate: nil, maxDate: nil), query: finalQuery, state: nil, limit: 50)] + searchSignals = [context.engine.messages.searchMessages(location: .general(scope: .globalPosts(allowPaidStars: approvedGlobalPostQueryState?.price), tags: nil, minDate: nil, maxDate: nil, folderId: 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) diff --git a/submodules/Components/PagerComponent/Sources/PagerComponent.swift b/submodules/Components/PagerComponent/Sources/PagerComponent.swift index 4b11ff0e5d..31f38da7cd 100644 --- a/submodules/Components/PagerComponent/Sources/PagerComponent.swift +++ b/submodules/Components/PagerComponent/Sources/PagerComponent.swift @@ -503,6 +503,20 @@ public final class PagerComponent UIView? { + if self.alpha.isZero { + return nil + } + for view in self.subviews.reversed() { + if let result = view.hitTest(self.convert(point, to: view), with: event), result.isUserInteractionEnabled { + return result + } + } + + let result = super.hitTest(point, with: event) + return result + } + func update(component: PagerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousPanelHideBehavior = self.component?.panelHideBehavior diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index 63f19d6cde..37137b665e 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -28,10 +28,12 @@ public final class NavigationBarTheme { public let badgeStrokeColor: UIColor public let badgeTextColor: UIColor public let edgeEffectColor: UIColor? + public let accentButtonColor: UIColor + public let accentForegroundColor: UIColor public let style: NavigationBar.Style public let glassStyle: NavigationBar.GlassStyle - public init(overallDarkAppearance: Bool, buttonColor: UIColor, disabledButtonColor: UIColor, primaryTextColor: UIColor, backgroundColor: UIColor, opaqueBackgroundColor: UIColor? = nil, enableBackgroundBlur: Bool, separatorColor: UIColor, badgeBackgroundColor: UIColor, badgeStrokeColor: UIColor, badgeTextColor: UIColor, edgeEffectColor: UIColor? = nil, style: NavigationBar.Style = .legacy, glassStyle: NavigationBar.GlassStyle = .default) { + public init(overallDarkAppearance: Bool, buttonColor: UIColor, disabledButtonColor: UIColor, primaryTextColor: UIColor, backgroundColor: UIColor, opaqueBackgroundColor: UIColor? = nil, enableBackgroundBlur: Bool, separatorColor: UIColor, badgeBackgroundColor: UIColor, badgeStrokeColor: UIColor, badgeTextColor: UIColor, edgeEffectColor: UIColor? = nil, accentButtonColor: UIColor, accentForegroundColor: UIColor, style: NavigationBar.Style = .legacy, glassStyle: NavigationBar.GlassStyle = .default) { self.overallDarkAppearance = overallDarkAppearance self.buttonColor = buttonColor self.disabledButtonColor = disabledButtonColor @@ -44,16 +46,18 @@ public final class NavigationBarTheme { self.badgeStrokeColor = badgeStrokeColor self.badgeTextColor = badgeTextColor self.edgeEffectColor = edgeEffectColor + self.accentButtonColor = accentButtonColor + self.accentForegroundColor = accentForegroundColor self.style = style self.glassStyle = glassStyle } public func withUpdatedBackgroundColor(_ color: UIColor) -> NavigationBarTheme { - return NavigationBarTheme(overallDarkAppearance: self.overallDarkAppearance, buttonColor: self.buttonColor, disabledButtonColor: self.disabledButtonColor, primaryTextColor: self.primaryTextColor, backgroundColor: color, opaqueBackgroundColor: self.opaqueBackgroundColor, enableBackgroundBlur: false, separatorColor: self.separatorColor, badgeBackgroundColor: self.badgeBackgroundColor, badgeStrokeColor: self.badgeStrokeColor, badgeTextColor: self.badgeTextColor, edgeEffectColor: self.edgeEffectColor, style: self.style, glassStyle: self.glassStyle) + return NavigationBarTheme(overallDarkAppearance: self.overallDarkAppearance, buttonColor: self.buttonColor, disabledButtonColor: self.disabledButtonColor, primaryTextColor: self.primaryTextColor, backgroundColor: color, opaqueBackgroundColor: self.opaqueBackgroundColor, enableBackgroundBlur: false, separatorColor: self.separatorColor, badgeBackgroundColor: self.badgeBackgroundColor, badgeStrokeColor: self.badgeStrokeColor, badgeTextColor: self.badgeTextColor, edgeEffectColor: self.edgeEffectColor, accentButtonColor: self.accentButtonColor, accentForegroundColor: self.accentForegroundColor, style: self.style, glassStyle: self.glassStyle) } public func withUpdatedSeparatorColor(_ color: UIColor) -> NavigationBarTheme { - return NavigationBarTheme(overallDarkAppearance: self.overallDarkAppearance, buttonColor: self.buttonColor, disabledButtonColor: self.disabledButtonColor, primaryTextColor: self.primaryTextColor, backgroundColor: self.backgroundColor, opaqueBackgroundColor: self.opaqueBackgroundColor, enableBackgroundBlur: self.enableBackgroundBlur, separatorColor: color, badgeBackgroundColor: self.badgeBackgroundColor, badgeStrokeColor: self.badgeStrokeColor, badgeTextColor: self.badgeTextColor, edgeEffectColor: self.edgeEffectColor, style: self.style, glassStyle: self.glassStyle) + return NavigationBarTheme(overallDarkAppearance: self.overallDarkAppearance, buttonColor: self.buttonColor, disabledButtonColor: self.disabledButtonColor, primaryTextColor: self.primaryTextColor, backgroundColor: self.backgroundColor, opaqueBackgroundColor: self.opaqueBackgroundColor, enableBackgroundBlur: self.enableBackgroundBlur, separatorColor: color, badgeBackgroundColor: self.badgeBackgroundColor, badgeStrokeColor: self.badgeStrokeColor, badgeTextColor: self.badgeTextColor, edgeEffectColor: self.edgeEffectColor, accentButtonColor: self.accentButtonColor, accentForegroundColor: self.accentForegroundColor, style: self.style, glassStyle: self.glassStyle) } } diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index ab140949c9..c4ae05271d 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -681,7 +681,7 @@ private func galleryEntriesForMessageHistoryEntries(_ entries: [MessageHistoryEn } public class GalleryController: ViewController, StandalonePresentableController, KeyShortcutResponder, GalleryControllerProtocol { - public static let darkNavigationTheme = NavigationBarTheme(overallDarkAppearance: true, buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: UIColor(white: 0.0, alpha: 0.6), enableBackgroundBlur: false, separatorColor: UIColor(white: 0.0, alpha: 0.8), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear, edgeEffectColor: .clear, style: .glass) + public static let darkNavigationTheme = NavigationBarTheme(overallDarkAppearance: true, buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: UIColor(white: 0.0, alpha: 0.6), enableBackgroundBlur: false, separatorColor: UIColor(white: 0.0, alpha: 0.8), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear, edgeEffectColor: .clear, accentButtonColor: .white, accentForegroundColor: .black, style: .glass) private var galleryNode: GalleryControllerNode { return self.displayNode as! GalleryControllerNode diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchGlobalChatContents.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchGlobalChatContents.swift index 3eabc0345c..c9869932c8 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchGlobalChatContents.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchGlobalChatContents.swift @@ -54,7 +54,7 @@ final class HashtagSearchGlobalChatContents: ChatCustomContentsProtocol { if self.publicPosts { search = self.context.engine.messages.searchHashtagPosts(hashtag: self.query, state: nil) } else { - search = self.context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: nil, minDate: nil, maxDate: nil), query: self.query, state: nil) + search = self.context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: nil, minDate: nil, maxDate: nil, folderId: nil), query: self.query, state: nil) } self.isSearchingPromise.set(true) @@ -102,7 +102,7 @@ final class HashtagSearchGlobalChatContents: ChatCustomContentsProtocol { if self.publicPosts { search = self.context.engine.messages.searchHashtagPosts(hashtag: self.query, state: self.currentSearchState) } else { - search = self.context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: nil, minDate: nil, maxDate: nil), query: self.query, state: currentSearchState) + search = self.context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: nil, minDate: nil, maxDate: nil, folderId: nil), query: self.query, state: currentSearchState) } self.historyViewDisposable?.dispose() diff --git a/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationController.swift b/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationController.swift index e6c938946d..56f4938f09 100644 --- a/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationController.swift +++ b/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationController.swift @@ -39,7 +39,7 @@ public class SetupTwoStepVerificationController: ViewController { self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(overallDarkAppearance: self.presentationData.theme.overallDarkAppearance, buttonColor: self.presentationData.theme.rootController.navigationBar.accentTextColor, disabledButtonColor: self.presentationData.theme.rootController.navigationBar.disabledButtonColor, primaryTextColor: self.presentationData.theme.rootController.navigationBar.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings))) + super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(overallDarkAppearance: self.presentationData.theme.overallDarkAppearance, buttonColor: self.presentationData.theme.rootController.navigationBar.accentTextColor, disabledButtonColor: self.presentationData.theme.rootController.navigationBar.disabledButtonColor, primaryTextColor: self.presentationData.theme.rootController.navigationBar.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear, accentButtonColor: self.presentationData.theme.list.itemCheckColors.fillColor, accentForegroundColor: self.presentationData.theme.list.itemCheckColors.foregroundColor), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings))) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style diff --git a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift index 54843c67d3..d42a2103b9 100644 --- a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift +++ b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift @@ -71,7 +71,7 @@ public final class TwoFactorDataInputScreen: ViewController { self.presentationData = self.sharedContext.currentPresentationData.with { $0 } let defaultTheme = NavigationBarTheme(rootControllerTheme: self.presentationData.theme) - let navigationBarTheme = NavigationBarTheme(overallDarkAppearance: defaultTheme.overallDarkAppearance, buttonColor: defaultTheme.buttonColor, disabledButtonColor: defaultTheme.disabledButtonColor, primaryTextColor: defaultTheme.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: defaultTheme.badgeBackgroundColor, badgeStrokeColor: defaultTheme.badgeStrokeColor, badgeTextColor: defaultTheme.badgeTextColor) + let navigationBarTheme = NavigationBarTheme(overallDarkAppearance: defaultTheme.overallDarkAppearance, buttonColor: defaultTheme.buttonColor, disabledButtonColor: defaultTheme.disabledButtonColor, primaryTextColor: defaultTheme.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: defaultTheme.badgeBackgroundColor, badgeStrokeColor: defaultTheme.badgeStrokeColor, badgeTextColor: defaultTheme.badgeTextColor, accentButtonColor: defaultTheme.accentButtonColor, accentForegroundColor: defaultTheme.accentForegroundColor) super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: self.presentationData.strings.Common_Back, close: self.presentationData.strings.Common_Close))) diff --git a/submodules/PasswordSetupUI/Sources/TwoFactorAuthSplashScreen.swift b/submodules/PasswordSetupUI/Sources/TwoFactorAuthSplashScreen.swift index 6b0209ed8c..f94f1ee2cf 100644 --- a/submodules/PasswordSetupUI/Sources/TwoFactorAuthSplashScreen.swift +++ b/submodules/PasswordSetupUI/Sources/TwoFactorAuthSplashScreen.swift @@ -57,7 +57,7 @@ public final class TwoFactorAuthSplashScreen: ViewController { self.presentationData = self.sharedContext.currentPresentationData.with { $0 } let defaultTheme = NavigationBarTheme(rootControllerTheme: self.presentationData.theme) - let navigationBarTheme = NavigationBarTheme(overallDarkAppearance: defaultTheme.overallDarkAppearance, buttonColor: defaultTheme.buttonColor, disabledButtonColor: defaultTheme.disabledButtonColor, primaryTextColor: defaultTheme.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: defaultTheme.badgeBackgroundColor, badgeStrokeColor: defaultTheme.badgeStrokeColor, badgeTextColor: defaultTheme.badgeTextColor) + let navigationBarTheme = NavigationBarTheme(overallDarkAppearance: defaultTheme.overallDarkAppearance, buttonColor: defaultTheme.buttonColor, disabledButtonColor: defaultTheme.disabledButtonColor, primaryTextColor: defaultTheme.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: defaultTheme.badgeBackgroundColor, badgeStrokeColor: defaultTheme.badgeStrokeColor, badgeTextColor: defaultTheme.badgeTextColor, accentButtonColor: defaultTheme.accentButtonColor, accentForegroundColor: defaultTheme.accentForegroundColor) super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: self.presentationData.strings.Common_Back, close: self.presentationData.strings.Common_Close))) diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 5180199ff7..c058ccfba1 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -1213,9 +1213,9 @@ public final class Transaction { self.postbox?.reindexUnreadCounters(currentTransaction: self) } - public func searchPeers(query: String) -> [RenderedPeer] { + public func searchPeers(query: String, predicate: ChatListFilterPredicate?) -> [RenderedPeer] { assert(!self.disposed) - return self.postbox?.searchPeers(query: query) ?? [] + return self.postbox?.searchPeers(transaction: self, query: query, predicate: predicate) ?? [] } public func clearTimestampBasedAttribute(id: MessageId, tag: UInt16) { @@ -3800,13 +3800,13 @@ final class PostboxImpl { } |> switchToLatest } - public func searchPeers(query: String) -> Signal<[RenderedPeer], NoError> { + public func searchPeers(query: String, predicate: ChatListFilterPredicate?) -> Signal<[RenderedPeer], NoError> { return self.transaction { transaction -> Signal<[RenderedPeer], NoError> in - return .single(transaction.searchPeers(query: query)) + return .single(transaction.searchPeers(query: query, predicate: predicate)) } |> switchToLatest } - fileprivate func searchPeers(query: String) -> [RenderedPeer] { + fileprivate func searchPeers(transaction: Transaction, query: String, predicate: ChatListFilterPredicate?) -> [RenderedPeer] { var peerIds = Set() var chatPeers: [RenderedPeer] = [] @@ -3823,6 +3823,30 @@ final class PostboxImpl { } chatPeerIds.append(contentsOf: additionalChatPeerIds) + if let predicate { + let globalNotificationSettings = self.getGlobalNotificationSettings(transaction: transaction) + + let filterImpl: (PeerId) -> Bool = { peerId in + guard let peer = self.peerTable.get(peerId) else { + return false + } + let inclusion = self.chatListIndexTable.get(peerId: peerId) + let isUnread = self.readStateTable.getCombinedState(peerId)?.isUnread ?? false + let notificationsPeerId = peer.notificationSettingsPeerId ?? peerId + let isContact = self.contactsTable.isContact(peerId: notificationsPeerId) + let isRemovedFromTotalUnreadCount = resolvedIsRemovedFromTotalUnreadCount(globalSettings: globalNotificationSettings, peer: peer, peerSettings: self.peerNotificationSettingsTable.getEffective(notificationsPeerId)) + let messageTagSummaryResult = resolveChatListMessageTagSummaryResultCalculation(postbox: self, peerId: peer.id, threadId: nil, calculation: predicate.messageTagSummary) + if predicate.includes(peer: peer, groupId: inclusion.inclusion.groupId ?? .root, isRemovedFromTotalUnreadCount: isRemovedFromTotalUnreadCount, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: messageTagSummaryResult) { + return true + } else { + return false + } + } + + chatPeerIds = chatPeerIds.filter(filterImpl) + contactPeerIds = contactPeerIds.filter(filterImpl) + } + for peerId in chatPeerIds { if let peer = self.peerTable.get(peerId) { var peers = SimpleDictionary() @@ -4929,12 +4953,12 @@ public class Postbox { } } - public func searchPeers(query: String) -> Signal<[RenderedPeer], NoError> { + public func searchPeers(query: String, predicate: ChatListFilterPredicate? = nil) -> Signal<[RenderedPeer], NoError> { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in - disposable.set(impl.searchPeers(query: query).start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion)) + disposable.set(impl.searchPeers(query: query, predicate: predicate).start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion)) } return disposable diff --git a/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift index ceab808e4a..7ede3135e3 100644 --- a/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift +++ b/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift @@ -77,7 +77,7 @@ public final class QrCodeScanScreen: ViewController { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - let navigationBarTheme = NavigationBarTheme(overallDarkAppearance: self.presentationData.theme.overallDarkAppearance, buttonColor: .white, disabledButtonColor: .white, primaryTextColor: .white, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear) + let navigationBarTheme = NavigationBarTheme(overallDarkAppearance: self.presentationData.theme.overallDarkAppearance, buttonColor: .white, disabledButtonColor: .white, primaryTextColor: .white, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear, accentButtonColor: .white, accentForegroundColor: .black) super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: self.presentationData.strings.Common_Back, close: self.presentationData.strings.Common_Close))) diff --git a/submodules/SearchBarNode/Sources/SearchBarNode.swift b/submodules/SearchBarNode/Sources/SearchBarNode.swift index bed3d19c0e..e585e4c3fb 100644 --- a/submodules/SearchBarNode/Sources/SearchBarNode.swift +++ b/submodules/SearchBarNode/Sources/SearchBarNode.swift @@ -928,6 +928,9 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate { } set { self.textField.tokens = newValue self.updateIsEmpty(animated: true) + if let (boundingSize, leftInset, rightInset) = self.validLayout { + self.updateLayout(boundingSize: boundingSize, leftInset: leftInset, rightInset: rightInset, transition: .immediate) + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift index c3ecf5a4e2..aedbdbeae3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift @@ -58,8 +58,8 @@ public extension TelegramEngine { return _internal_searchPeers(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, query: query, scope: scope) } - public func searchLocalPeers(query: String, scope: TelegramSearchPeersScope = .everywhere) -> Signal<[EngineRenderedPeer], NoError> { - return self.account.postbox.searchPeers(query: query) + public func searchLocalPeers(query: String, scope: TelegramSearchPeersScope = .everywhere, predicate: ChatListFilterPredicate? = nil) -> Signal<[EngineRenderedPeer], NoError> { + return self.account.postbox.searchPeers(query: query, predicate: predicate) |> map { peers in switch scope { case .everywhere: diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift index d89275a8ed..f385472317 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift @@ -6,7 +6,7 @@ import MtProtoKit public enum SearchMessagesLocation: Equatable { - case general(scope: TelegramSearchPeersScope, tags: MessageTags?, minDate: Int32?, maxDate: Int32?) + case general(scope: TelegramSearchPeersScope, tags: MessageTags?, minDate: Int32?, maxDate: Int32?, folderId: Int32?) case group(groupId: PeerGroupId, tags: MessageTags?, minDate: Int32?, maxDate: Int32?) case peer(peerId: PeerId, fromId: PeerId?, tags: MessageTags?, reactions: [MessageReaction.Reaction]?, threadId: Int64?, minDate: Int32?, maxDate: Int32?) case sentMedia(tags: MessageTags?) @@ -471,7 +471,7 @@ func _internal_searchMessages(account: Account, location: SearchMessagesLocation } return combineLatest(peerMessages, additionalPeerMessages) } - case let .general(_, tags, minDate, maxDate), let .group(_, tags, minDate, maxDate): + case let .general(_, tags, minDate, maxDate, _), let .group(_, tags, minDate, maxDate): var flags: Int32 = 0 let folderId: Int32? if case let .group(groupId, _, _, _) = location { @@ -481,7 +481,7 @@ func _internal_searchMessages(account: Account, location: SearchMessagesLocation folderId = nil } - if case let .general(scope, _, _, _) = location, case let .globalPosts(allowPaidStars) = 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 { @@ -508,7 +508,7 @@ func _internal_searchMessages(account: Account, location: SearchMessagesLocation } } } else { - if case let .general(scope, _, _, _) = location { + if case let .general(scope, _, _, _, _) = location { switch scope { case .everywhere: break @@ -567,9 +567,9 @@ func _internal_searchMessages(account: Account, location: SearchMessagesLocation if state?.additional == nil { switch location { - case let .general(_, tags, minDate, maxDate), let .group(_, tags, minDate, maxDate): + case let .general(_, tags, minDate, maxDate, _), let .group(_, tags, minDate, maxDate): let secretMessages: [Message] - if case let .general(scope, _, _, _) = location, case .channels = scope { + if case let .general(scope, _, _, _, _) = location, case .channels = scope { secretMessages = [] } else { secretMessages = transaction.searchMessages(peerId: nil, query: query, tags: tags) diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index 731e31b609..7a7ac6d024 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -208,6 +208,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case giftCraftingTips = 85 case copyProtectionTips = 86 case aiTextProcessingStyleSelectionTips = 87 + case savedMessagesChatListView = 88 var key: ValueBoxKey { let v = ValueBoxKey(length: 4) @@ -589,6 +590,10 @@ private struct ApplicationSpecificNoticeKeys { static func aiTextProcessingStyleSelectionTips() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.aiTextProcessingStyleSelectionTips.key) } + + static func savedMessagesChatListView() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.savedMessagesChatListView.key) + } } public struct ApplicationSpecificNotice { @@ -2610,4 +2615,31 @@ public struct ApplicationSpecificNotice { return Int(previousValue) } } + + public static func getSavedMessagesChatListView(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Int32 in + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.savedMessagesChatListView())?.get(ApplicationSpecificCounterNotice.self) { + return value.value + } else { + return 0 + } + } + } + + public static func incrementSavedMessagesChatListView(accountManager: AccountManager, count: Int = 1) -> Signal { + return accountManager.transaction { transaction -> Int in + var currentValue: Int32 = 0 + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.savedMessagesChatListView())?.get(ApplicationSpecificCounterNotice.self) { + currentValue = value.value + } + let previousValue = currentValue + currentValue += Int32(count) + + if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) { + transaction.setNotice(ApplicationSpecificNoticeKeys.savedMessagesChatListView(), entry) + } + + return Int(previousValue) + } + } } diff --git a/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift b/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift index 0f9f22faa0..8dbd3e82b6 100644 --- a/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift +++ b/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift @@ -69,7 +69,7 @@ public extension NavigationBarTheme { badgeTextColor = theme.badgeTextColor } - self.init(overallDarkAppearance: rootControllerTheme.overallDarkAppearance, buttonColor: buttonColor, disabledButtonColor: disabledButtonColor, primaryTextColor: theme.primaryTextColor, backgroundColor: hideBackground ? .clear : theme.blurredBackgroundColor, opaqueBackgroundColor: hideBackground ? .clear : theme.opaqueBackgroundColor, enableBackgroundBlur: enableBackgroundBlur, separatorColor: hideBackground || hideSeparator ? .clear : theme.separatorColor, badgeBackgroundColor: hideBadge ? .clear : badgeBackgroundColor, badgeStrokeColor: .clear, badgeTextColor: hideBadge ? .clear : badgeTextColor, edgeEffectColor: edgeEffectColor, style: style, glassStyle: glassStyle) + self.init(overallDarkAppearance: rootControllerTheme.overallDarkAppearance, buttonColor: buttonColor, disabledButtonColor: disabledButtonColor, primaryTextColor: theme.primaryTextColor, backgroundColor: hideBackground ? .clear : theme.blurredBackgroundColor, opaqueBackgroundColor: hideBackground ? .clear : theme.opaqueBackgroundColor, enableBackgroundBlur: enableBackgroundBlur, separatorColor: hideBackground || hideSeparator ? .clear : theme.separatorColor, badgeBackgroundColor: hideBadge ? .clear : badgeBackgroundColor, badgeStrokeColor: .clear, badgeTextColor: hideBadge ? .clear : badgeTextColor, edgeEffectColor: edgeEffectColor, accentButtonColor: rootControllerTheme.list.itemCheckColors.fillColor, accentForegroundColor: rootControllerTheme.list.itemCheckColors.foregroundColor, style: style, glassStyle: glassStyle) } } diff --git a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift index d69c236735..e387d05658 100644 --- a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift +++ b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift @@ -756,7 +756,7 @@ public func makeAttachmentFileControllerImpl( case .audio: recentDocuments = .single(nil) |> then( - context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: [.music], minDate: nil, maxDate: nil), query: "", state: nil) + context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: [.music], minDate: nil, maxDate: nil, folderId: nil), query: "", state: nil) |> map { result -> [Message]? in return result.0.messages } diff --git a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift index f3edb35f99..2e1463429e 100644 --- a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift +++ b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift @@ -530,7 +530,7 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon case .audio: shared = .single(nil) |> then( - context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: [.music], minDate: nil, maxDate: nil), query: query, state: nil) + context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: [.music], minDate: nil, maxDate: nil, folderId: nil), query: query, state: nil) |> delay(0.6, queue: Queue.mainQueue()) |> map { result -> [Message]? in return result.0.messages.filter { !$0.isRestricted(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) } diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index a15805b502..2b3544686b 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -361,7 +361,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { private let context: AccountContext private let stateContext: StateContext? - private let entityKeyboardView: ComponentHostView + private let entityKeyboardView: ComponentView private let defaultToEmojiTab: Bool private var stableReorderableGroupOrder: [EntityKeyboardComponent.ReorderCategory: [ItemCollectionId]] = [:] @@ -449,7 +449,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } public var canSwitchToTextInputAutomatically: Bool { - if let pagerView = self.entityKeyboardView.componentView as? EntityKeyboardComponent.View, let centralId = pagerView.centralId { + if let pagerView = self.entityKeyboardView.view as? EntityKeyboardComponent.View, let centralId = pagerView.centralId { if centralId == AnyHashable("emoji") { return false } @@ -496,11 +496,11 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { self.interaction = interaction - self.clippingView = UIView() + self.clippingView = SparseContainerView() self.clippingView.clipsToBounds = true self.clippingView.layer.cornerRadius = keyboardCornerRadius - self.entityKeyboardView = ComponentHostView() + self.entityKeyboardView = ComponentView() super.init() @@ -538,7 +538,6 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { ) } - self.clippingView.addSubview(self.entityKeyboardView) self.view.addSubview(self.clippingView) if let backgroundChromeView = self.backgroundChromeView { @@ -692,7 +691,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { for featuredStickerPack in stickerPacks { if featuredStickerPack.topItems.contains(where: { $0.file.fileId == file.fileId }) { - if let pagerView = self.entityKeyboardView.componentView as? EntityKeyboardComponent.View, let emojiInputInteraction = self.emojiInputInteraction { + if let pagerView = self.entityKeyboardView.view as? EntityKeyboardComponent.View, let emojiInputInteraction = self.emojiInputInteraction { pagerView.openCustomSearch(content: EmojiSearchContent( context: self.context, forceTheme: self.interaction?.forceTheme, @@ -1326,7 +1325,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { )) }, openSearch: { [weak self] in - if let strongSelf = self, let pagerView = strongSelf.entityKeyboardView.componentView as? EntityKeyboardComponent.View { + if let strongSelf = self, let pagerView = strongSelf.entityKeyboardView.view as? EntityKeyboardComponent.View { pagerView.openSearch() } }, @@ -1630,7 +1629,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { var transition: ComponentTransition = .immediate var useAnimation = false - if let pagerView = strongSelf.entityKeyboardView.componentView as? EntityKeyboardComponent.View, let centralId = pagerView.centralId { + if let pagerView = strongSelf.entityKeyboardView.view as? EntityKeyboardComponent.View, let centralId = pagerView.centralId { if centralId == AnyHashable("emoji") { useAnimation = strongSelf.currentInputData.emoji != inputData.emoji } else if centralId == AnyHashable("stickers"), strongSelf.currentInputData.stickers != nil, inputData.stickers != nil { @@ -1710,7 +1709,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { gifContext.loadMore(token: token) }, openSearch: { [weak self] in - if let strongSelf = self, let pagerView = strongSelf.entityKeyboardView.componentView as? EntityKeyboardComponent.View { + if let strongSelf = self, let pagerView = strongSelf.entityKeyboardView.view as? EntityKeyboardComponent.View { pagerView.openSearch() } }, @@ -1798,8 +1797,13 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if let result = super.hitTest(point, with: event) { - return result + if self.alpha.isZero || !self.view.isUserInteractionEnabled { + return nil + } + for subview in self.view.subviews.reversed() { + if let result = subview.hitTest(self.view.convert(point, to: subview), with: event), result.isUserInteractionEnabled { + return result + } } if let backgroundView = self.backgroundView, backgroundView.frame.contains(point) { @@ -2025,7 +2029,12 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { entityKeyboardSizeFrame.origin.y += self.topBackgroundExtension } - transition.updateFrame(view: self.entityKeyboardView, frame: entityKeyboardSizeFrame) + if let entityKeyboardComponentView = self.entityKeyboardView.view { + if entityKeyboardComponentView.superview == nil { + self.clippingView.addSubview(entityKeyboardComponentView) + } + transition.updateFrame(view: entityKeyboardComponentView, frame: entityKeyboardSizeFrame) + } transition.updateFrame(view: self.clippingView, frame: clippingFrame) @@ -2366,7 +2375,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } public func scrollToGroupEmoji() { - if let pagerView = self.entityKeyboardView.componentView as? EntityKeyboardComponent.View { + if let pagerView = self.entityKeyboardView.view as? EntityKeyboardComponent.View { pagerView.scrollToItemGroup(contentId: "emoji", groupId: "peerSpecific", subgroupId: nil) } } diff --git a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift index c49ef78263..782a1ff862 100644 --- a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift +++ b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift @@ -157,7 +157,8 @@ private final class ChatScheduleTimeSheetContentComponent: Component { self.environment = environment if self.component == nil { - if case .format = component.mode { + switch component.mode { + case .format, .search: self.minDate = Date(timeIntervalSince1970: 0.0) self.maxDate = Date(timeIntervalSince1970: Double(Int32.max - 1)) if let currentTime = component.currentTime { @@ -167,7 +168,7 @@ private final class ChatScheduleTimeSheetContentComponent: Component { } else { self.date = Date() } - } else { + default: self.updateMinimumDate(currentTime: component.currentTime, minimalTime: component.minimalTime) } self.repeatPeriod = component.currentRepeatPeriod @@ -227,6 +228,9 @@ private final class ChatScheduleTimeSheetContentComponent: Component { title = strings.Conversation_FormatDate_Title case .poll: title = strings.CreatePoll_Deadline_Title + case .search: + //TODO:localize + title = "Search" } let titleSize = self.title.update( transition: transition, @@ -386,6 +390,8 @@ private final class ChatScheduleTimeSheetContentComponent: Component { var repeatValueFrame = CGRect() if case .format = component.mode { contentHeight += 8.0 + } else if case .search = component.mode { + contentHeight += 8.0 } else if case .poll = component.mode { contentHeight += 8.0 } else { @@ -497,6 +503,9 @@ private final class ChatScheduleTimeSheetContentComponent: Component { buttonTitle = component.currentTime != nil ? strings.Conversation_FormatDate_EditDate : strings.Conversation_FormatDate_AddDate case .poll: buttonTitle = strings.CreatePoll_Deadline_SetDeadline + case .search: + //TODO:localize + buttonTitle = "Done" } let buttonSideInset: CGFloat = 30.0 @@ -539,7 +548,13 @@ private final class ChatScheduleTimeSheetContentComponent: Component { } contentHeight += buttonSize.height - if case .format = component.mode, component.currentTime != nil { + var isFormatOrSearch = false + if case .format = component.mode { + isFormatOrSearch = true + } else if case .search = component.mode { + isFormatOrSearch = true + } + if isFormatOrSearch && component.currentTime != nil { contentHeight += 8.0 let buttonSize = self.secondaryButton.update( @@ -947,6 +962,7 @@ public class ChatScheduleTimeScreen: ViewControllerComponentContainer { case reminders case format case poll + case search } public struct Result { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index 12ec0cfb7f..c525591808 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -272,7 +272,7 @@ public final class EntityKeyboardComponent: Component { public final class View: UIView { private let tintContainerView: UIView - private let pagerView: ComponentHostView + private let pagerView: ComponentView private var component: EntityKeyboardComponent? public private(set) weak var state: EmptyComponentState? @@ -295,20 +295,32 @@ public final class EntityKeyboardComponent: Component { override init(frame: CGRect) { self.tintContainerView = UIView() - self.pagerView = ComponentHostView() + self.pagerView = ComponentView() super.init(frame: frame) //self.clipsToBounds = true self.disablesInteractiveTransitionGestureRecognizer = true - - self.addSubview(self.pagerView) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.alpha.isZero { + return nil + } + for view in self.subviews.reversed() { + if let result = view.hitTest(self.convert(point, to: view), with: event), result.isUserInteractionEnabled { + return result + } + } + + let result = super.hitTest(point, with: event) + return result + } + func update(component: EntityKeyboardComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.state = state @@ -787,7 +799,12 @@ public final class EntityKeyboardComponent: Component { forceUpdate: forceUpdate, containerSize: availableSize ) - transition.setFrame(view: self.pagerView, frame: CGRect(origin: CGPoint(), size: pagerSize)) + if let pagerComponentView = self.pagerView.view { + if pagerComponentView.superview == nil { + self.insertSubview(pagerComponentView, at: 0) + } + transition.setFrame(view: pagerComponentView, frame: CGRect(origin: CGPoint(), size: pagerSize)) + } let accountContext = component.emojiContent?.context ?? component.stickerContent?.context if let searchComponent = self.searchComponent, let accountContext = accountContext { diff --git a/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift b/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift index bf2eaacaa4..8639fa514d 100644 --- a/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift +++ b/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift @@ -441,9 +441,7 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { self.leftButtonNodeImpl.view.removeFromSuperview() var backTitle: String? - if case .glass = self.presentationData.theme.style { - backTitle = "" - } else if let customBackButtonText = self.customBackButtonText { + if let customBackButtonText = self.customBackButtonText { backTitle = customBackButtonText } else if let leftBarButtonItem = item.leftBarButtonItem, leftBarButtonItem.backButtonAppearance { backTitle = leftBarButtonItem.title @@ -460,6 +458,12 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { } } + if backTitle != nil { + if case .glass = self.presentationData.theme.style { + backTitle = "" + } + } + if let backTitle { self.backButtonNodeImpl.updateManualText(backTitle, isBack: true) if self.backButtonNodeImpl.supernode == nil { @@ -631,10 +635,6 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { self.backButtonNodeImpl.color = self.presentationData.theme.buttonColor self.backButtonNodeImpl.disabledColor = self.presentationData.theme.disabledButtonColor - self.leftButtonNodeImpl.color = self.presentationData.theme.buttonColor - self.leftButtonNodeImpl.disabledColor = self.presentationData.theme.disabledButtonColor - self.rightButtonNodeImpl.color = self.presentationData.theme.buttonColor - self.rightButtonNodeImpl.disabledColor = self.presentationData.theme.disabledButtonColor self.backButtonArrow.image = presentationData.theme.style == .glass ? generateTintedImage(image: glassBackArrowImage, color: self.presentationData.theme.buttonColor) : navigationBarBackArrowImage(color: self.presentationData.theme.buttonColor) if let title = self.title { self.titleNode.attributedText = NSAttributedString(string: title, font: NavigationBarImpl.titleFont, textColor: self.presentationData.theme.primaryTextColor) @@ -753,10 +753,6 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { self.backButtonNodeImpl.color = self.presentationData.theme.buttonColor self.backButtonNodeImpl.disabledColor = self.presentationData.theme.disabledButtonColor - self.leftButtonNodeImpl.color = self.presentationData.theme.buttonColor - self.leftButtonNodeImpl.disabledColor = self.presentationData.theme.disabledButtonColor - self.rightButtonNodeImpl.color = self.presentationData.theme.buttonColor - self.rightButtonNodeImpl.disabledColor = self.presentationData.theme.disabledButtonColor self.backButtonArrow.image = self.presentationData.theme.style == .glass ? generateTintedImage(image: glassBackArrowImage, color: self.presentationData.theme.buttonColor) : navigationBarBackArrowImage(color: self.presentationData.theme.buttonColor) if let title = self.title { self.titleNode.attributedText = NSAttributedString(string: title, font: NavigationBarImpl.titleFont, textColor: self.presentationData.theme.primaryTextColor) @@ -906,6 +902,15 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { self.badgeNode.alpha = 1.0 } } else if self.leftButtonNodeImpl.view.superview != nil { + switch self.leftButtonNodeImpl.commonContentType { + case .accent: + self.leftButtonNodeImpl.color = self.presentationData.theme.accentForegroundColor + self.leftButtonNodeImpl.disabledColor = self.presentationData.theme.accentForegroundColor.withMultipliedAlpha(0.5) + case .generic: + self.leftButtonNodeImpl.color = self.presentationData.theme.buttonColor + self.leftButtonNodeImpl.disabledColor = self.presentationData.theme.disabledButtonColor + } + let leftButtonSize = self.leftButtonNodeImpl.updateLayout(constrainedSize: CGSize(width: size.width, height: 44.0), isLandscape: isLandscape, isLeftAligned: true) leftTitleInset = leftButtonSize.width + leftButtonInset + 1.0 @@ -937,6 +942,15 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { var rightButtonsWidth: CGFloat = 0.0 if self.rightButtonNodeImpl.view.superview != nil { + switch self.rightButtonNodeImpl.commonContentType { + case .accent: + self.rightButtonNodeImpl.color = self.presentationData.theme.accentForegroundColor + self.rightButtonNodeImpl.disabledColor = self.presentationData.theme.accentForegroundColor.withMultipliedAlpha(0.5) + case .generic: + self.rightButtonNodeImpl.color = self.presentationData.theme.buttonColor + self.rightButtonNodeImpl.disabledColor = self.presentationData.theme.disabledButtonColor + } + let rightButtonSize = self.rightButtonNodeImpl.updateLayout(constrainedSize: (CGSize(width: size.width, height: 44.0)), isLandscape: isLandscape, isLeftAligned: false) if !self.rightButtonNodeImpl.isEmpty { rightButtonsWidth += rightButtonSize.width @@ -975,7 +989,16 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { leftButtonsBackgroundTransition.setBounds(view: leftButtonsBackgroundView.background, bounds: CGRect(origin: CGPoint(), size: leftButtonsBackgroundFrame.size)) leftButtonsBackgroundTransition.setFrame(view: leftButtonsBackgroundView.container, frame: CGRect(origin: CGPoint(), size: leftButtonsBackgroundFrame.size)) ComponentTransition(transition).setAlpha(view: leftButtonsBackgroundView.background, alpha: leftButtonsWidth == 0.0 ? 0.0 : 1.0) - leftButtonsBackgroundView.background.update(size: leftButtonsBackgroundFrame.size, cornerRadius: leftButtonsBackgroundFrame.height * 0.5, isDark: self.presentationData.theme.overallDarkAppearance, tintColor: .init(kind: self.presentationData.theme.glassStyle == .clear ? .clear : .panel), isInteractive: true, isVisible: leftButtonsWidth != 0.0, transition: leftButtonsBackgroundTransition) + + var leftButtonsColor: GlassBackgroundView.TintColor = .init(kind: self.presentationData.theme.glassStyle == .clear ? .clear : .panel) + switch self.leftButtonNodeImpl.commonContentType { + case .accent: + leftButtonsColor = .init(kind: .custom(style: self.presentationData.theme.glassStyle == .clear ? .clear : .default, color: self.presentationData.theme.accentButtonColor)) + case .generic: + break + } + + leftButtonsBackgroundView.background.update(size: leftButtonsBackgroundFrame.size, cornerRadius: leftButtonsBackgroundFrame.height * 0.5, isDark: self.presentationData.theme.overallDarkAppearance, tintColor: leftButtonsColor, isInteractive: true, isVisible: leftButtonsWidth != 0.0, transition: leftButtonsBackgroundTransition) } if let rightButtonsBackgroundView = self.rightButtonsBackgroundView { @@ -1001,8 +1024,16 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { }) } + var rightButtonsColor: GlassBackgroundView.TintColor = .init(kind: self.presentationData.theme.glassStyle == .clear ? .clear : .panel) + switch self.rightButtonNodeImpl.commonContentType { + case .accent: + rightButtonsColor = .init(kind: .custom(style: self.presentationData.theme.glassStyle == .clear ? .clear : .default, color: self.presentationData.theme.accentButtonColor)) + case .generic: + break + } + rightButtonsBackgroundView.background.isHidden = false - rightButtonsBackgroundView.background.update(size: rightButtonsBackgroundFrame.size, cornerRadius: rightButtonsBackgroundFrame.height * 0.5, isDark: self.presentationData.theme.overallDarkAppearance, tintColor: .init(kind: self.presentationData.theme.glassStyle == .clear ? .clear : .panel), isInteractive: true, transition: rightButtonsBackgroundTransition) + rightButtonsBackgroundView.background.update(size: rightButtonsBackgroundFrame.size, cornerRadius: rightButtonsBackgroundFrame.height * 0.5, isDark: self.presentationData.theme.overallDarkAppearance, tintColor: rightButtonsColor, isInteractive: true, transition: rightButtonsBackgroundTransition) } else { rightButtonsBackgroundView.background.isHidden = true } @@ -1038,7 +1069,7 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { titleViewTransition = .immediate } - let titleSize = titleView.updateLayout(availableSize: CGSize(width: size.width - max(leftTitleInset, rightTitleInset) * 2.0, height: nominalHeight), transition: titleViewTransition) + let titleSize = titleView.updateLayout(availableSize: CGSize(width: size.width - leftTitleInset - rightTitleInset, height: nominalHeight), transition: titleViewTransition) var titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) * 0.5), y: contentVerticalOrigin + floorToScreenPixels((nominalHeight - titleSize.height) / 2.0)), size: titleSize) if titleFrame.origin.x + titleFrame.width > size.width - rightTitleInset { @@ -1050,8 +1081,14 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { titleViewTransition.updateFrame(view: titleView, frame: titleFrame) } else { - let titleSize = CGSize(width: max(1.0, size.width - max(leftTitleInset, rightTitleInset) * 2.0), height: nominalHeight) - let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: contentVerticalOrigin + floorToScreenPixels((nominalHeight - titleSize.height) / 2.0)), size: titleSize) + let titleSize = CGSize(width: max(1.0, size.width - leftTitleInset - rightTitleInset), height: nominalHeight) + var titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: contentVerticalOrigin + floorToScreenPixels((nominalHeight - titleSize.height) / 2.0)), size: titleSize) + if titleFrame.origin.x + titleFrame.width > size.width - rightTitleInset { + titleFrame.origin.x = size.width - rightTitleInset - titleFrame.width + } + if titleFrame.origin.x < leftTitleInset { + titleFrame.origin.x = leftTitleInset + floorToScreenPixels((size.width - leftTitleInset - rightTitleInset - titleFrame.width) * 0.5) + } var titleViewTransition = transition if titleView.frame.isEmpty { titleViewTransition = .immediate diff --git a/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationButtonNode.swift b/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationButtonNode.swift index 0446f196a0..5c5982341f 100644 --- a/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationButtonNode.swift +++ b/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationButtonNode.swift @@ -261,7 +261,7 @@ private final class NavigationButtonItemNode: ImmediateTextNode { } } - private(set) var imageNode: ASImageNode? + private(set) var imageView: UIImageView? private var _image: UIImage? public var image: UIImage? { @@ -271,23 +271,21 @@ private final class NavigationButtonItemNode: ImmediateTextNode { _image = value if let _ = value { - if self.imageNode == nil { - let imageNode = ASImageNode() - imageNode.displayWithoutProcessing = true - imageNode.displaysAsynchronously = false - self.imageNode = imageNode + if self.imageView == nil { + let imageView = UIImageView() + self.imageView = imageView - self.addSubnode(imageNode) + self.view.addSubview(imageView) } - self.imageNode?.image = image - if self.imageNode?.image?.renderingMode == .alwaysTemplate { - self.imageNode?.tintColor = self.color + self.imageView?.image = image + if self.imageView?.image?.renderingMode == .alwaysTemplate { + self.imageView?.tintColor = self.color } else { - self.imageNode?.tintColor = nil + self.imageView?.tintColor = nil } - } else if let imageNode = self.imageNode { - imageNode.removeFromSupernode() - self.imageNode = nil + } else if let imageView = self.imageView { + imageView.removeFromSuperview() + self.imageView = nil } self.invalidateCalculatedLayout() @@ -311,11 +309,15 @@ private final class NavigationButtonItemNode: ImmediateTextNode { public var color: UIColor = UIColor(rgb: 0x0088ff) { didSet { - if self.imageNode?.image?.renderingMode == .alwaysTemplate { - self.imageNode?.tintColor = self.color - } - if let text = self._text { - self.attributedText = NSAttributedString(string: text, attributes: self.attributesForCurrentState()) + if self.color != oldValue { + if self.imageView?.image?.renderingMode == .alwaysTemplate { + self.imageView?.tintColor = self.color + } else { + self.image = self.image + } + if let text = self._text { + self.attributedText = NSAttributedString(string: text, attributes: self.attributesForCurrentState()) + } } } } @@ -422,11 +424,11 @@ private final class NavigationButtonItemNode: ImmediateTextNode { let size = CGSize(width: max(nodeSize.width, superSize.width), height: max(nodeSize.height, superSize.height)) node.frame = CGRect(origin: CGPoint(), size: nodeSize) return size - } else if let imageNode = self.imageNode { - let nodeSize = imageNode.image?.size ?? CGSize() + } else if let imageView = self.imageView { + let nodeSize = imageView.image?.size ?? CGSize() let size = CGSize(width: max(nodeSize.width, superSize.width), height: max(44.0, max(nodeSize.height, superSize.height))) let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - nodeSize.width) / 2.0), y: floorToScreenPixels((size.height - nodeSize.height) / 2.0)), size: nodeSize) - imageNode.frame = imageFrame + imageView.frame = imageFrame return size } else { superSize.height = max(44.0, superSize.height) @@ -524,9 +526,15 @@ private final class NavigationButtonItemNode: ImmediateTextNode { public final class NavigationButtonNodeImpl: ContextControllerSourceNode, NavigationButtonNode { + enum ContentType { + case accent + case generic + } + private let isGlass: Bool private var isBack: Bool = false + private var items: [UIBarButtonItem] = [] private var nodes: [NavigationButtonItemNode] = [] private var disappearingNodes: [(frame: CGRect, size: CGSize, node: NavigationButtonItemNode)] = [] @@ -648,6 +656,8 @@ public final class NavigationButtonNodeImpl: ContextControllerSourceNode, Naviga } public func updateItems(_ items: [UIBarButtonItem], animated: Bool) { + self.items = items + for i in 0 ..< items.count { let node: NavigationButtonItemNode if self.nodes.count > i { @@ -676,8 +686,11 @@ public final class NavigationButtonNodeImpl: ContextControllerSourceNode, Naviga node.alpha = self.manualAlpha node.item = items[i] if items[i].title == "___close" { - //node.image = glassCloseImage - node.image = generateTintedImage(image: UIImage(bundleImageName: "Navigation/Close"), color: self.color) + node.image = generateTintedImage(image: UIImage(bundleImageName: "Navigation/Close"), color: .white)?.withRenderingMode(.alwaysTemplate) + } else if items[i].title == "___clear" { + node.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: .white)?.withRenderingMode(.alwaysTemplate) + } else if items[i].title == "___done" { + node.image = generateTintedImage(image: UIImage(bundleImageName: "Navigation/Done"), color: .white)?.withRenderingMode(.alwaysTemplate) } else { node.image = items[i].image node.text = items[i].title ?? "" @@ -749,7 +762,7 @@ public final class NavigationButtonNodeImpl: ContextControllerSourceNode, Naviga nodeOrigin.x += 16.0 } - if !self.isGlass && node.node == nil && node.imageNode != nil && i == self.nodes.count - 1 { + if !self.isGlass && node.node == nil && node.imageView != nil && i == self.nodes.count - 1 { nodeOrigin.x -= 5.0 } } @@ -791,4 +804,26 @@ public final class NavigationButtonNodeImpl: ContextControllerSourceNode, Naviga } return true } + + var commonContentType: ContentType { + var commonType: ContentType? + for item in self.items { + var nodeContentType: ContentType = .generic + if item.title == "___close" { + } else if item.title == "___clear" { + } else if item.title == "___done" { + nodeContentType = .accent + } + + if commonType == nil { + commonType = nodeContentType + } else { + if commonType != nodeContentType { + return .generic + } + } + } + + return commonType ?? .generic + } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 256f17800c..ab03540a15 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -4949,7 +4949,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self?.deactivateSearch() }, fieldStyle: .glass) } else if let currentPaneKey = self.paneContainerNode.currentPaneKey, case .savedMessagesChats = currentPaneKey { - let contentNode = ChatListSearchContainerNode(context: self.context, animationCache: self.context.animationCache, animationRenderer: self.context.animationRenderer, filter: [.removeSearchHeader], requestPeerType: nil, location: .savedMessagesChats(peerId: self.context.account.peerId), displaySearchFilters: false, hasDownloads: false, initialFilter: .chats, openPeer: { [weak self] peer, _, _, _ in + let contentNode = ChatListSearchContainerNode(context: self.context, animationCache: self.context.animationCache, animationRenderer: self.context.animationRenderer, filter: [.removeSearchHeader], requestPeerType: nil, location: .savedMessagesChats(peerId: self.context.account.peerId), folder: nil, displaySearchFilters: false, hasDownloads: false, initialFilter: .chats, openPeer: { [weak self] peer, _, _, _ in guard let self else { return } @@ -6460,7 +6460,9 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc separatorColor: .clear, badgeBackgroundColor: baseNavigationBarPresentationData.theme.badgeBackgroundColor, badgeStrokeColor: baseNavigationBarPresentationData.theme.badgeStrokeColor, - badgeTextColor: baseNavigationBarPresentationData.theme.badgeTextColor + badgeTextColor: baseNavigationBarPresentationData.theme.badgeTextColor, + accentButtonColor: baseNavigationBarPresentationData.theme.accentButtonColor, + accentForegroundColor: baseNavigationBarPresentationData.theme.accentForegroundColor ), strings: baseNavigationBarPresentationData.strings)) self._hasGlassStyle = true diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index aedd7a31fa..b438e2e511 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -1330,6 +1330,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { filter: self.filter, requestPeerType: self.requestPeerType, location: chatListLocation, + folder: nil, displaySearchFilters: false, hasDownloads: false, openPeer: { [weak self] peer, chatPeer, threadId, _ in diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 568d6bf79b..1a5ff0c4bb 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -1172,7 +1172,7 @@ extension ChatControllerImpl { } return updatedState }) - self.searchResult.set(.single((results, state, .general(scope: .channels, tags: nil, minDate: nil, maxDate: nil)))) + self.searchResult.set(.single((results, state, .general(scope: .channels, tags: nil, minDate: nil, maxDate: nil, folderId: nil)))) } } @@ -2412,7 +2412,7 @@ extension ChatControllerImpl { } } }, openCalendarSearch: { [weak self] in - self?.openCalendarSearch(timestamp: Int32(Date().timeIntervalSince1970)) + self?.openCalendarSearch(timestamp: Int32(Date().timeIntervalSince1970), isMedia: false) }, toggleMembersSearch: { [weak self] value in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 3269558e13..b7293406a9 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -3468,7 +3468,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G switch strongSelf.chatLocation { case let .peer(peerId): if alreadyThere { - strongSelf.openCalendarSearch(timestamp: timestamp) + strongSelf.openCalendarSearch(timestamp: timestamp, isMedia: true) } else { strongSelf.navigateToMessage(from: nil, to: .index(MessageIndex(id: MessageId(peerId: peerId, namespace: 0, id: 0), timestamp: timestamp - Int32(NSTimeZone.local.secondsFromGMT()))), scrollPosition: .bottom(0.0), rememberInStack: false, animated: true, completion: nil) } diff --git a/submodules/TelegramUI/Sources/ChatControllerContentData.swift b/submodules/TelegramUI/Sources/ChatControllerContentData.swift index c7661a559a..0d6d1b2b6d 100644 --- a/submodules/TelegramUI/Sources/ChatControllerContentData.swift +++ b/submodules/TelegramUI/Sources/ChatControllerContentData.swift @@ -14,6 +14,7 @@ import ChatListUI import EmojiStatusComponent import TelegramUIPreferences import TranslateUI +import TelegramNotices extension ChatControllerImpl { final class ContentData { @@ -507,6 +508,25 @@ extension ChatControllerImpl { messageOptionsTitleInfo = .single(nil) } + var savedMessagesChatsTip: Signal = .single(false) + if case .peer(context.account.peerId) = chatLocation { + let hasSavedChats = context.engine.messages.savedMessagesHasPeersOtherThanSaved() + if chatLocation.threadId == nil { + savedMessagesChatsTip = hasSavedChats + |> distinctUntilChanged + |> mapToSignal { value -> Signal in + if !value { + return .single(false) + } + return ApplicationSpecificNotice.getSavedMessagesChatListView(accountManager: context.sharedContext.accountManager) + |> map { value -> Bool in + return value < 5 + } + } + |> distinctUntilChanged + } + } + self.titleDisposable = (combineLatest( queue: Queue.mainQueue(), peerView.get(), @@ -515,9 +535,10 @@ extension ChatControllerImpl { subtitleTextSignal, configuration, hasPeerInfo, - messageOptionsTitleInfo + messageOptionsTitleInfo, + savedMessagesChatsTip ) - |> deliverOnMainQueue).startStrict(next: { [weak self] peerView, onlineMemberCount, displayedCount, subtitleText, configuration, hasPeerInfo, messageOptionsTitleInfo in + |> deliverOnMainQueue).startStrict(next: { [weak self] peerView, onlineMemberCount, displayedCount, subtitleText, configuration, hasPeerInfo, messageOptionsTitleInfo, savedMessagesChatsTip in guard let strongSelf = self else { return } @@ -586,7 +607,13 @@ extension ChatControllerImpl { strongSelf.state.chatTitleContent = .custom(title: [ChatTitleContent.TitleTextItem(id: AnyHashable(0), content: .text(channel.debugDisplayTitle))], subtitle: nil, isEnabled: true) } } else { - strongSelf.state.chatTitleContent = .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: nil, customSubtitle: nil, onlineMemberCount: onlineMemberCount, isScheduledMessages: isScheduledMessages, isMuted: nil, customMessageCount: nil, isEnabled: hasPeerInfo) + var customSubtitle: String? + if savedMessagesChatsTip { + //TODO:localize + customSubtitle = "Tap to view as chats" + } + + strongSelf.state.chatTitleContent = .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: nil, customSubtitle: customSubtitle, onlineMemberCount: onlineMemberCount, isScheduledMessages: isScheduledMessages, isMuted: nil, customMessageCount: nil, isEnabled: hasPeerInfo) let imageOverride: AvatarNodeImageOverride? if context.account.peerId == peer.id { diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift b/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift index 81ead39eac..821488a047 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift @@ -10,83 +10,48 @@ import ChatControllerInteraction import Display import UIKit import UndoUI +import ChatScheduleTimeController extension ChatControllerImpl { - func openCalendarSearch(timestamp: Int32) { + func openCalendarSearch(timestamp: Int32, isMedia: Bool) { guard let peerId = self.chatLocation.peerId else { return } self.chatDisplayNode.dismissInput() - - let initialTimestamp = timestamp - var dismissCalendarScreen: (() -> Void)? - var selectDay: ((Int32) -> Void)? - var openClearHistory: ((Int32) -> Void)? - - let enableMessageRangeDeletion: Bool = peerId.namespace == Namespaces.Peer.CloudUser - let displayMedia = self.presentationInterfaceState.historyFilter == nil - - let calendarScreen = CalendarMessageScreen( - context: self.context, - peerId: peerId, - calendarSource: self.context.engine.messages.sparseMessageCalendar(peerId: peerId, threadId: self.chatLocation.threadId, tag: .photoOrVideo, displayMedia: displayMedia), - initialTimestamp: initialTimestamp, - enableMessageRangeDeletion: enableMessageRangeDeletion, - canNavigateToEmptyDays: true, - navigateToDay: { [weak self] c, index, timestamp in - guard let strongSelf = self else { - c.dismiss() - return - } - - strongSelf.alwaysShowSearchResultsAsList = false - strongSelf.chatDisplayNode.alwaysShowSearchResultsAsList = false - strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in - return state.updatedDisplayHistoryFilterAsList(false).updatedSearch(nil) - }) - - c.dismiss() - - strongSelf.loadingMessage.set(.single(.generic)) - - let peerId: PeerId - let threadId: Int64? - switch strongSelf.chatLocation { - case let .peer(peerIdValue): - peerId = peerIdValue - threadId = nil - case let .replyThread(replyThreadMessage): - peerId = replyThreadMessage.peerId - threadId = replyThreadMessage.threadId - case .customChatContents: - return - } - - strongSelf.messageIndexDisposable.set((strongSelf.context.engine.messages.searchMessageIdByTimestamp(peerId: peerId, threadId: threadId, timestamp: timestamp) |> deliverOnMainQueue).startStrict(next: { messageId in - if let strongSelf = self { - strongSelf.loadingMessage.set(.single(nil)) - if let messageId = messageId { - strongSelf.navigateToMessage(from: nil, to: .id(messageId, NavigateToMessageParams(timestamp: nil, quote: nil)), forceInCurrentChat: true) - } + if isMedia { + let initialTimestamp = timestamp + var dismissCalendarScreen: (() -> Void)? + var selectDay: ((Int32) -> Void)? + var openClearHistory: ((Int32) -> Void)? + + let enableMessageRangeDeletion: Bool = peerId.namespace == Namespaces.Peer.CloudUser + + let displayMedia = self.presentationInterfaceState.historyFilter == nil + + let calendarScreen = CalendarMessageScreen( + context: self.context, + peerId: peerId, + calendarSource: self.context.engine.messages.sparseMessageCalendar(peerId: peerId, threadId: self.chatLocation.threadId, tag: .photoOrVideo, displayMedia: displayMedia), + initialTimestamp: initialTimestamp, + enableMessageRangeDeletion: enableMessageRangeDeletion, + canNavigateToEmptyDays: true, + navigateToDay: { [weak self] c, index, timestamp in + guard let strongSelf = self else { + c.dismiss() + return } - })) - }, - previewDay: { [weak self] timestamp, _, sourceNode, sourceRect, gesture in - guard let strongSelf = self else { - return - } - - var items: [ContextMenuItem] = [] - - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Chat_JumpToDate, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) - }, action: { _, f in - f(.dismissWithoutContent) - dismissCalendarScreen?() - + + strongSelf.alwaysShowSearchResultsAsList = false + strongSelf.chatDisplayNode.alwaysShowSearchResultsAsList = false + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in + return state.updatedDisplayHistoryFilterAsList(false).updatedSearch(nil) + }) + + c.dismiss() + strongSelf.loadingMessage.set(.single(.generic)) - + let peerId: PeerId let threadId: Int64? switch strongSelf.chatLocation { @@ -99,7 +64,7 @@ extension ChatControllerImpl { case .customChatContents: return } - + strongSelf.messageIndexDisposable.set((strongSelf.context.engine.messages.searchMessageIdByTimestamp(peerId: peerId, threadId: threadId, timestamp: timestamp) |> deliverOnMainQueue).startStrict(next: { messageId in if let strongSelf = self { strongSelf.loadingMessage.set(.single(nil)) @@ -108,84 +73,168 @@ extension ChatControllerImpl { } } })) - }))) - - if enableMessageRangeDeletion && (peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.SecretChat) { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.DialogList_ClearHistoryConfirmation, textColor: .destructive, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + }, + previewDay: { [weak self] timestamp, _, sourceNode, sourceRect, gesture in + guard let strongSelf = self else { + return + } + + var items: [ContextMenuItem] = [] + + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Chat_JumpToDate, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { _, f in f(.dismissWithoutContent) - openClearHistory?(timestamp) - }))) - - items.append(.separator) - - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Select, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) - }, action: { _, f in - f(.dismissWithoutContent) - selectDay?(timestamp) - }))) - } - - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: .message(id: .timestamp(timestamp), highlight: nil, timecode: nil, setupReply: false), botStart: nil, mode: .standard(.previewing), params: nil) - chatController.canReadHistory.set(false) - - strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() - - let contextController = makeContextController(presentationData: strongSelf.presentationData, source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, sourceRect: sourceRect, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) - strongSelf.presentInGlobalOverlay(contextController) - } - ) - - calendarScreen.completedWithRemoveMessagesInRange = { [weak self] range, type, dayCount, calendarSource in - guard let strongSelf = self else { - return - } - - let statusText: String - switch type { - case .forEveryone: - statusText = strongSelf.presentationData.strings.Chat_MessageRangeDeleted_ForBothSides(Int32(dayCount)) - default: - statusText = strongSelf.presentationData.strings.Chat_MessageRangeDeleted_ForMe(Int32(dayCount)) - } - - strongSelf.chatDisplayNode.historyNode.ignoreMessagesInTimestampRange = range - - strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: strongSelf.context, title: NSAttributedString(string: statusText), text: nil), elevatedLayout: false, action: { value in - guard let strongSelf = self else { - return false - } - - if value == .commit { - let _ = calendarSource.removeMessagesInRange(minTimestamp: range.lowerBound, maxTimestamp: range.upperBound, type: type, completion: { - Queue.mainQueue().after(1.0, { - guard let strongSelf = self else { - return + dismissCalendarScreen?() + + strongSelf.loadingMessage.set(.single(.generic)) + + let peerId: PeerId + let threadId: Int64? + switch strongSelf.chatLocation { + case let .peer(peerIdValue): + peerId = peerIdValue + threadId = nil + case let .replyThread(replyThreadMessage): + peerId = replyThreadMessage.peerId + threadId = replyThreadMessage.threadId + case .customChatContents: + return + } + + strongSelf.messageIndexDisposable.set((strongSelf.context.engine.messages.searchMessageIdByTimestamp(peerId: peerId, threadId: threadId, timestamp: timestamp) |> deliverOnMainQueue).startStrict(next: { messageId in + if let strongSelf = self { + strongSelf.loadingMessage.set(.single(nil)) + if let messageId = messageId { + strongSelf.navigateToMessage(from: nil, to: .id(messageId, NavigateToMessageParams(timestamp: nil, quote: nil)), forceInCurrentChat: true) + } } - strongSelf.chatDisplayNode.historyNode.ignoreMessagesInTimestampRange = nil - }) - }) - return true - } else if value == .undo { - strongSelf.chatDisplayNode.historyNode.ignoreMessagesInTimestampRange = nil - return true + })) + }))) + + if enableMessageRangeDeletion && (peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.SecretChat) { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.DialogList_ClearHistoryConfirmation, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + }, action: { _, f in + f(.dismissWithoutContent) + openClearHistory?(timestamp) + }))) + + items.append(.separator) + + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Select, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.dismissWithoutContent) + selectDay?(timestamp) + }))) + } + + let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: .message(id: .timestamp(timestamp), highlight: nil, timecode: nil, setupReply: false), botStart: nil, mode: .standard(.previewing), params: nil) + chatController.canReadHistory.set(false) + + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + + let contextController = makeContextController(presentationData: strongSelf.presentationData, source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, sourceRect: sourceRect, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + strongSelf.presentInGlobalOverlay(contextController) } - return false - }), in: .current) - } - - self.effectiveNavigationController?.pushViewController(calendarScreen) - - dismissCalendarScreen = { [weak calendarScreen] in - calendarScreen?.dismiss(completion: nil) - } - selectDay = { [weak calendarScreen] timestamp in - calendarScreen?.selectDay(timestamp: timestamp) - } - openClearHistory = { [weak calendarScreen] timestamp in - calendarScreen?.openClearHistory(timestamp: timestamp) + ) + + calendarScreen.completedWithRemoveMessagesInRange = { [weak self] range, type, dayCount, calendarSource in + guard let strongSelf = self else { + return + } + + let statusText: String + switch type { + case .forEveryone: + statusText = strongSelf.presentationData.strings.Chat_MessageRangeDeleted_ForBothSides(Int32(dayCount)) + default: + statusText = strongSelf.presentationData.strings.Chat_MessageRangeDeleted_ForMe(Int32(dayCount)) + } + + strongSelf.chatDisplayNode.historyNode.ignoreMessagesInTimestampRange = range + + strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: strongSelf.context, title: NSAttributedString(string: statusText), text: nil), elevatedLayout: false, action: { value in + guard let strongSelf = self else { + return false + } + + if value == .commit { + let _ = calendarSource.removeMessagesInRange(minTimestamp: range.lowerBound, maxTimestamp: range.upperBound, type: type, completion: { + Queue.mainQueue().after(1.0, { + guard let strongSelf = self else { + return + } + strongSelf.chatDisplayNode.historyNode.ignoreMessagesInTimestampRange = nil + }) + }) + return true + } else if value == .undo { + strongSelf.chatDisplayNode.historyNode.ignoreMessagesInTimestampRange = nil + return true + } + return false + }), in: .current) + } + + self.effectiveNavigationController?.pushViewController(calendarScreen) + + dismissCalendarScreen = { [weak calendarScreen] in + calendarScreen?.dismiss(completion: nil) + } + selectDay = { [weak calendarScreen] timestamp in + calendarScreen?.selectDay(timestamp: timestamp) + } + openClearHistory = { [weak calendarScreen] timestamp in + calendarScreen?.openClearHistory(timestamp: timestamp) + } + } else { + let controller = ChatScheduleTimeScreen( + context: self.context, + mode: .search, + currentTime: nil, + currentRepeatPeriod: nil, + minimalTime: nil, + isDark: false, + completion: { [weak self] result in + guard let self else { + return + } + + self.alwaysShowSearchResultsAsList = false + self.chatDisplayNode.alwaysShowSearchResultsAsList = false + self.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in + return state.updatedDisplayHistoryFilterAsList(false).updatedSearch(nil) + }) + + self.loadingMessage.set(.single(.generic)) + + let peerId: PeerId + let threadId: Int64? + switch self.chatLocation { + case let .peer(peerIdValue): + peerId = peerIdValue + threadId = nil + case let .replyThread(replyThreadMessage): + peerId = replyThreadMessage.peerId + threadId = replyThreadMessage.threadId + case .customChatContents: + return + } + + self.messageIndexDisposable.set((self.context.engine.messages.searchMessageIdByTimestamp(peerId: peerId, threadId: threadId, timestamp: result.time) |> deliverOnMainQueue).startStrict(next: { [weak self] messageId in + guard let self else { + return + } + self.loadingMessage.set(.single(nil)) + if let messageId { + self.navigateToMessage(from: nil, to: .id(messageId, NavigateToMessageParams(timestamp: nil, quote: nil)), forceInCurrentChat: true) + } + })) + } + ) + self.push(controller) } } } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift index 82c58604fa..803f089285 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift @@ -47,10 +47,14 @@ func leftNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Cha } if canClear { - return ChatNavigationButton(action: .clearHistory, buttonItem: UIBarButtonItem(title: title, style: .plain, target: target, action: selector)) + let buttonItem = UIBarButtonItem(title: "___clear", style: .plain, target: target, action: selector) + buttonItem.accessibilityLabel = title + return ChatNavigationButton(action: .clearHistory, buttonItem: buttonItem) } else { title = strings.Conversation_ClearCache - return ChatNavigationButton(action: .clearCache, buttonItem: UIBarButtonItem(title: title, style: .plain, target: target, action: selector)) + let buttonItem = UIBarButtonItem(title: "___clear", style: .plain, target: target, action: selector) + buttonItem.accessibilityLabel = title + return ChatNavigationButton(action: .clearCache, buttonItem: buttonItem) } } } @@ -88,7 +92,7 @@ func rightNavigationButtonForChatInterfaceState(context: AccountContext, present if let currentButton = currentButton, currentButton.action == .cancelMessageSelection { return currentButton } else { - let buttonItem = UIBarButtonItem(title: strings.Common_Cancel, style: .plain, target: target, action: selector) + let buttonItem = UIBarButtonItem(title: "___done", style: .plain, target: target, action: selector) buttonItem.accessibilityLabel = strings.Common_Cancel return ChatNavigationButton(action: .cancelMessageSelection, buttonItem: buttonItem) } diff --git a/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift b/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift index b6d8b61f14..48266cf949 100644 --- a/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift +++ b/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift @@ -60,7 +60,7 @@ final class WebSearchGalleryControllerPresentationArguments { } class WebSearchGalleryController: ViewController { - private static let navigationTheme = NavigationBarTheme(overallDarkAppearance: false, buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear) + private static let navigationTheme = NavigationBarTheme(overallDarkAppearance: false, buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear, accentButtonColor: .white, accentForegroundColor: .black) private var galleryNode: GalleryControllerNode { return self.displayNode as! GalleryControllerNode diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 7e0ce945ea..7c217e478c 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -3800,7 +3800,9 @@ public final class WebAppController: ViewController, AttachmentContainable { separatorColor: UIColor(rgb: 0x000000, alpha: 0.25), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, - badgeTextColor: .clear + badgeTextColor: .clear, + accentButtonColor: self.presentationData.theme.list.itemCheckColors.fillColor, + accentForegroundColor: self.presentationData.theme.list.itemCheckColors.foregroundColor ), strings: NavigationBarStrings(back: "", close: "") ) From 0fecd9a5356a9a3f304d60bc716d636797d3d270 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 31 Mar 2026 15:38:13 +0200 Subject: [PATCH 07/50] Fix --- .../ComposePollScreen/Sources/ComposePollScreen.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift b/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift index c7f9fdcb36..a76df13afe 100644 --- a/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift +++ b/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift @@ -543,7 +543,7 @@ final class ComposePollScreenComponent: Component { } var mappedSolution: (String, [MessageTextEntity], AnyMediaReference?)? - if self.isQuiz && self.quizAnswerTextInputState.text.length != 0 { + if self.isQuiz && (self.quizAnswerTextInputState.text.length != 0 || self.quizAnswerMedia != nil) { var solutionTextEntities: [MessageTextEntity] = [] for entity in generateChatInputTextEntities(self.quizAnswerTextInputState.text) { switch entity.type { From 796826b0addc6ada4d676610f22df37efddebe87 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 31 Mar 2026 22:42:02 +0800 Subject: [PATCH 08/50] Various improvements --- .../Sources/PeerSelectionController.swift | 5 +- .../Sources/Node/ChatListNode.swift | 731 ++++++++++-------- .../Sources/Node/ChatListNodeEntries.swift | 36 +- .../Sources/PeerSelectionController.swift | 4 +- .../Sources/PeerSelectionControllerNode.swift | 6 +- .../Components/StorageUsageScreen/BUILD | 2 + .../StorageFileListPanelComponent.swift | 102 ++- .../StoragePeerListPanelComponent.swift | 95 ++- .../StorageUsagePanelContainerComponent.swift | 416 +++------- .../Sources/StorageUsageScreen.swift | 14 +- .../Chat/ChatMessageActionOptions.swift | 157 ++-- .../ContactMultiselectionControllerNode.swift | 2 +- 12 files changed, 766 insertions(+), 804 deletions(-) diff --git a/submodules/AccountContext/Sources/PeerSelectionController.swift b/submodules/AccountContext/Sources/PeerSelectionController.swift index 2a63824a1f..547d98a0e6 100644 --- a/submodules/AccountContext/Sources/PeerSelectionController.swift +++ b/submodules/AccountContext/Sources/PeerSelectionController.swift @@ -67,6 +67,7 @@ public final class PeerSelectionControllerParams { public let hasCreation: Bool public let immediatelySwitchToContacts: Bool public let immediatelyActivateMultipleSelection: Bool + public let suggestedPeers: [EnginePeer] public init( context: AccountContext, @@ -89,7 +90,8 @@ public final class PeerSelectionControllerParams { selectForumThreads: Bool = false, hasCreation: Bool = false, immediatelySwitchToContacts: Bool = false, - immediatelyActivateMultipleSelection: Bool = false + immediatelyActivateMultipleSelection: Bool = false, + suggestedPeers: [EnginePeer] = [] ) { self.context = context self.updatedPresentationData = updatedPresentationData @@ -112,6 +114,7 @@ public final class PeerSelectionControllerParams { self.hasCreation = hasCreation self.immediatelySwitchToContacts = immediatelySwitchToContacts self.immediatelyActivateMultipleSelection = immediatelyActivateMultipleSelection + self.suggestedPeers = suggestedPeers } } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 4236fb048a..75a63892a7 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -27,7 +27,7 @@ import GlobalControlPanelsContext public enum ChatListNodeMode { case chatList(appendContacts: Bool) - case peers(filter: ChatListNodePeersFilter, isSelecting: Bool, additionalCategories: [ChatListNodeAdditionalCategory], chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool, displayPresence: Bool) + case peers(filter: ChatListNodePeersFilter, isSelecting: Bool, additionalCategories: [ChatListNodeAdditionalCategory], topPeers: [EnginePeer], chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool, displayPresence: Bool) case peerType(type: [ReplyMarkupButtonRequestPeerType], hasCreate: Bool) } @@ -372,286 +372,268 @@ public struct ChatListNodeState: Equatable { } } -private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, isPremium: Bool, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, entries: [ChatListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] { +private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, isPremium: Bool, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, entries: [ChatListNodeViewTransitionInsertEntry], presentationData: ChatListPresentationData) -> [ListViewInsertItem] { return entries.map { entry -> ListViewInsertItem in switch entry.entry { - case .HeaderEntry: - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyHeaderItem(), directionHint: entry.directionHint) - case let .AdditionalCategory(_, id, title, image, appearance, selected, presentationData): - var header: ChatListSearchItemHeader? - if case .peerType = mode { - } else { - if case .action = appearance { - // TODO: hack, generalize - header = ChatListSearchItemHeader(type: .orImportIntoAnExistingGroup, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) - } + case .HeaderEntry: + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyHeaderItem(), directionHint: entry.directionHint) + case let .AdditionalCategory(_, id, title, image, appearance, selected, presentationData): + var header: ChatListSearchItemHeader? + if case .peerType = mode { + } else { + if case .action = appearance { + // TODO: hack, generalize + header = ChatListSearchItemHeader(type: .orImportIntoAnExistingGroup, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) } - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListAdditionalCategoryItem( - presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat), - context: context, - title: title, - image: image, - appearance: appearance, - isSelected: selected, - header: header, - action: { - nodeInteraction.additionalCategorySelected(id) - } - ), directionHint: entry.directionHint) - case let .PeerEntry(peerEntry): - let index = peerEntry.index - let presentationData = peerEntry.presentationData - let combinedReadState = peerEntry.readState - let isRemovedFromTotalUnreadCount = peerEntry.isRemovedFromTotalUnreadCount - let draftState = peerEntry.draftState - let peer = peerEntry.peer - let threadInfo = peerEntry.threadInfo - let presence = peerEntry.presence - let hasUnseenMentions = peerEntry.hasUnseenMentions - let hasUnseenReactions = peerEntry.hasUnseenReactions - let hasUnseenPollVotes = peerEntry.hasUnseenPollVotes - let editing = peerEntry.editing - let hasActiveRevealControls = peerEntry.hasActiveRevealControls - let selected = peerEntry.selected - let inputActivities = peerEntry.inputActivities - let promoInfo = peerEntry.promoInfo - let hasFailedMessages = peerEntry.hasFailedMessages - let isContact = peerEntry.isContact - let forumTopicData = peerEntry.forumTopicData - let topForumTopicItems = peerEntry.topForumTopicItems - let revealed = peerEntry.revealed - - switch mode { - case .chatList: - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem( - presentationData: presentationData, - context: context, - chatListLocation: location, - filterData: filterData, - index: index, - content: .peer(ChatListItemContent.PeerData( - messages: peerEntry.messages, - peer: peer, - threadInfo: threadInfo, - combinedReadState: combinedReadState, - isRemovedFromTotalUnreadCount: isRemovedFromTotalUnreadCount, - presence: presence, - hasUnseenMentions: hasUnseenMentions, - hasUnseenReactions: hasUnseenReactions, - hasUnseenPollVotes: hasUnseenPollVotes, - draftState: draftState, - mediaDraftContentType: peerEntry.mediaDraftContentType, - inputActivities: inputActivities, - promoInfo: promoInfo, - ignoreUnreadBadge: false, - displayAsMessage: false, - hasFailedMessages: hasFailedMessages, - forumTopicData: forumTopicData, - topForumTopicItems: topForumTopicItems, - autoremoveTimeout: peerEntry.autoremoveTimeout, - storyState: peerEntry.storyState.flatMap { storyState in - return ChatListItemContent.StoryState( - stats: storyState.stats, - hasUnseenCloseFriends: storyState.hasUnseenCloseFriends - ) - }, - requiresPremiumForMessaging: peerEntry.requiresPremiumForMessaging, - displayAsTopicList: peerEntry.displayAsTopicList, - tags: chatListItemTags(location: location, accountPeerId: context.account.peerId, isPremium: isPremium, peer: peer.chatMainPeer, isUnread: combinedReadState?.isUnread ?? false, isMuted: isRemovedFromTotalUnreadCount, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: chatListFilters) - )), - editing: editing, - hasActiveRevealControls: hasActiveRevealControls, - selected: selected, - header: nil, - enabledContextActions: .auto, - hiddenOffset: threadInfo?.isHidden == true && !revealed, - interaction: nodeInteraction - ), directionHint: entry.directionHint) - case let .peers(filter, isSelecting, _, filters, displayAutoremoveTimeout, displayPresence): - let itemPeer = peer.chatOrMonoforumMainPeer - var chatPeer: EnginePeer? - if let peer = peer.peers[peer.peerId] { - chatPeer = peer - } - var enabled = true - if let isPeerEnabled { - if let itemPeer { - enabled = isPeerEnabled(itemPeer) - } - } else { - if filter.contains(.onlyWriteable) { - if let peer = peer.peers[peer.peerId] { - if !canSendMessagesToPeer(peer._asPeer()) { - enabled = false - } - if peerEntry.requiresPremiumForMessaging { - enabled = false - } - } else { - enabled = false - } - - if let threadInfo, threadInfo.isClosed, case let .channel(channel) = itemPeer { - if threadInfo.isOwnedByMe || channel.hasPermission(.manageTopics) { - } else { - enabled = false - } - } - } - if filter.contains(.onlyPrivateChats) { - if let peer = peer.peers[peer.peerId] { - switch peer { - case .user, .secretChat: - break - default: - enabled = false - } - } else { - enabled = false - } - } - if filter.contains(.onlyGroups) { - if let peer = peer.peers[peer.peerId] { - if case .legacyGroup = peer { - } else if case let .channel(peer) = peer, case .group = peer.info { - } else { - enabled = false - } - } else { - enabled = false - } - } - if filter.contains(.onlyManageable) { - if let peer = peer.peers[peer.peerId] { - var canManage = false - if case let .legacyGroup(peer) = peer { - switch peer.role { - case .creator, .admin: - canManage = true - default: - break - } - } else if case let .channel(peer) = peer, case .group = peer.info, peer.hasPermission(.inviteMembers) { - canManage = true - } else if case let .channel(peer) = peer, case .broadcast = peer.info, peer.hasPermission(.addAdmins) { - canManage = true - } - - if canManage { - } else { - enabled = false - } - } else { - enabled = false - } - } - if filter.contains(.excludeChannels) { - if let peer = peer.peers[peer.peerId] { - if case let .channel(peer) = peer, case .broadcast = peer.info { - enabled = false - } - } - } - } - - var header: ChatListSearchItemHeader? - switch mode { - case let .peers(_, _, additionalCategories, _, _, _): - if !additionalCategories.isEmpty { - let headerType: ChatListSearchItemHeaderType - if case .action = additionalCategories[0].appearance { - // TODO: hack, generalize - headerType = .orImportIntoAnExistingGroup - } else { - headerType = .chats - } - header = ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) - } - default: - break - } - - var status: ContactsPeerItemStatus = .none - if isSelecting, let itemPeer = itemPeer { - if displayPresence, let presence = presence { - status = .presence(presence, presentationData.dateTimeFormat) - } else if let (string, multiline, isActive, icon) = statusStringForPeerType(accountPeerId: context.account.peerId, strings: presentationData.strings, peer: itemPeer, isMuted: isRemovedFromTotalUnreadCount, isUnread: combinedReadState?.isUnread ?? false, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: filters, displayAutoremoveTimeout: displayAutoremoveTimeout, autoremoveTimeout: peerEntry.autoremoveTimeout) { - status = .custom(string: string, multiline: multiline, isActive: isActive, icon: icon) - } else { - status = .none - } - } - - let peerContent: ContactsPeerItemPeer - if let threadInfo = threadInfo, let itemPeer = itemPeer { - peerContent = .thread(peer: itemPeer, title: threadInfo.info.title, icon: threadInfo.info.icon, color: threadInfo.info.iconColor) - } else { - peerContent = .peer(peer: itemPeer, chatPeer: chatPeer) - } - - var threadId: Int64? - switch index { - case let .forum(_, _, threadIdValue, _, _): - threadId = threadIdValue - case .chatList: - break - } - - var isForum = false - if let peer = chatPeer, case let .channel(channel) = peer, channel.isForumOrMonoForum { - isForum = true - if editing, case .chatList = mode { - enabled = false - } - } - - var selectable = editing - if case .chatList = mode { - if isForum { - selectable = false - } - } - - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem( - presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat), - sortOrder: presentationData.nameSortOrder, - displayOrder: presentationData.nameDisplayOrder, - context: context, - peerMode: .generalSearch(isSavedMessages: false), - peer: peerContent, - status: status, - requiresPremiumForMessaging: peerEntry.requiresPremiumForMessaging, - enabled: enabled, - selection: selectable ? .selectable(selected: selected) : .none, - editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), - index: nil, - header: header, - action: { _ in - if let chatPeer = chatPeer { - if editing { - nodeInteraction.togglePeerSelected(chatPeer, threadId) - } else { - nodeInteraction.peerSelected(chatPeer, nil, threadId, nil, false) - } - } - }, disabledAction: (isForum && editing) && !peerEntry.requiresPremiumForMessaging ? nil : { _ in - if let chatPeer = chatPeer { - nodeInteraction.disabledPeerSelected(chatPeer, threadId, peerEntry.requiresPremiumForMessaging ? .premiumRequired : .generic) - } + } + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListAdditionalCategoryItem( + presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat), + context: context, + title: title, + image: image, + appearance: appearance, + isSelected: selected, + header: header, + action: { + nodeInteraction.additionalCategorySelected(id) + } + ), directionHint: entry.directionHint) + case let .TopPeer(_, peer): + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem( + presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat), + sortOrder: presentationData.nameSortOrder, + displayOrder: presentationData.nameDisplayOrder, + context: context, + peerMode: .generalSearch(isSavedMessages: false), + peer: .peer(peer: peer, chatPeer: peer), + status: .none, + requiresPremiumForMessaging: false, + enabled: true, + selection: .none, + editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), + index: nil, + header: nil, + action: { _ in + nodeInteraction.peerSelected(peer, nil, nil, nil, false) + }, + disabledAction: nil, + animationCache: nodeInteraction.animationCache, + animationRenderer: nodeInteraction.animationRenderer + ), directionHint: entry.directionHint) + case let .PeerEntry(peerEntry): + let index = peerEntry.index + let presentationData = peerEntry.presentationData + let combinedReadState = peerEntry.readState + let isRemovedFromTotalUnreadCount = peerEntry.isRemovedFromTotalUnreadCount + let draftState = peerEntry.draftState + let peer = peerEntry.peer + let threadInfo = peerEntry.threadInfo + let presence = peerEntry.presence + let hasUnseenMentions = peerEntry.hasUnseenMentions + let hasUnseenReactions = peerEntry.hasUnseenReactions + let hasUnseenPollVotes = peerEntry.hasUnseenPollVotes + let editing = peerEntry.editing + let hasActiveRevealControls = peerEntry.hasActiveRevealControls + let selected = peerEntry.selected + let inputActivities = peerEntry.inputActivities + let promoInfo = peerEntry.promoInfo + let hasFailedMessages = peerEntry.hasFailedMessages + let isContact = peerEntry.isContact + let forumTopicData = peerEntry.forumTopicData + let topForumTopicItems = peerEntry.topForumTopicItems + let revealed = peerEntry.revealed + + switch mode { + case .chatList: + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem( + presentationData: presentationData, + context: context, + chatListLocation: location, + filterData: filterData, + index: index, + content: .peer(ChatListItemContent.PeerData( + messages: peerEntry.messages, + peer: peer, + threadInfo: threadInfo, + combinedReadState: combinedReadState, + isRemovedFromTotalUnreadCount: isRemovedFromTotalUnreadCount, + presence: presence, + hasUnseenMentions: hasUnseenMentions, + hasUnseenReactions: hasUnseenReactions, + hasUnseenPollVotes: hasUnseenPollVotes, + draftState: draftState, + mediaDraftContentType: peerEntry.mediaDraftContentType, + inputActivities: inputActivities, + promoInfo: promoInfo, + ignoreUnreadBadge: false, + displayAsMessage: false, + hasFailedMessages: hasFailedMessages, + forumTopicData: forumTopicData, + topForumTopicItems: topForumTopicItems, + autoremoveTimeout: peerEntry.autoremoveTimeout, + storyState: peerEntry.storyState.flatMap { storyState in + return ChatListItemContent.StoryState( + stats: storyState.stats, + hasUnseenCloseFriends: storyState.hasUnseenCloseFriends + ) }, - animationCache: nodeInteraction.animationCache, - animationRenderer: nodeInteraction.animationRenderer - ), directionHint: entry.directionHint) - case .peerType: + requiresPremiumForMessaging: peerEntry.requiresPremiumForMessaging, + displayAsTopicList: peerEntry.displayAsTopicList, + tags: chatListItemTags(location: location, accountPeerId: context.account.peerId, isPremium: isPremium, peer: peer.chatMainPeer, isUnread: combinedReadState?.isUnread ?? false, isMuted: isRemovedFromTotalUnreadCount, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: chatListFilters) + )), + editing: editing, + hasActiveRevealControls: hasActiveRevealControls, + selected: selected, + header: nil, + enabledContextActions: .auto, + hiddenOffset: threadInfo?.isHidden == true && !revealed, + interaction: nodeInteraction + ), directionHint: entry.directionHint) + case let .peers(filter, isSelecting, _, _, filters, displayAutoremoveTimeout, displayPresence): let itemPeer = peer.chatOrMonoforumMainPeer var chatPeer: EnginePeer? if let peer = peer.peers[peer.peerId] { chatPeer = peer } + var enabled = true + if let isPeerEnabled { + if let itemPeer { + enabled = isPeerEnabled(itemPeer) + } + } else { + if filter.contains(.onlyWriteable) { + if let peer = peer.peers[peer.peerId] { + if !canSendMessagesToPeer(peer._asPeer()) { + enabled = false + } + if peerEntry.requiresPremiumForMessaging { + enabled = false + } + } else { + enabled = false + } + + if let threadInfo, threadInfo.isClosed, case let .channel(channel) = itemPeer { + if threadInfo.isOwnedByMe || channel.hasPermission(.manageTopics) { + } else { + enabled = false + } + } + } + if filter.contains(.onlyPrivateChats) { + if let peer = peer.peers[peer.peerId] { + switch peer { + case .user, .secretChat: + break + default: + enabled = false + } + } else { + enabled = false + } + } + if filter.contains(.onlyGroups) { + if let peer = peer.peers[peer.peerId] { + if case .legacyGroup = peer { + } else if case let .channel(peer) = peer, case .group = peer.info { + } else { + enabled = false + } + } else { + enabled = false + } + } + if filter.contains(.onlyManageable) { + if let peer = peer.peers[peer.peerId] { + var canManage = false + if case let .legacyGroup(peer) = peer { + switch peer.role { + case .creator, .admin: + canManage = true + default: + break + } + } else if case let .channel(peer) = peer, case .group = peer.info, peer.hasPermission(.inviteMembers) { + canManage = true + } else if case let .channel(peer) = peer, case .broadcast = peer.info, peer.hasPermission(.addAdmins) { + canManage = true + } + + if canManage { + } else { + enabled = false + } + } else { + enabled = false + } + } + if filter.contains(.excludeChannels) { + if let peer = peer.peers[peer.peerId] { + if case let .channel(peer) = peer, case .broadcast = peer.info { + enabled = false + } + } + } + } - let peerContent: ContactsPeerItemPeer = .peer(peer: itemPeer, chatPeer: chatPeer) - let status: ContactsPeerItemStatus = .none + var header: ChatListSearchItemHeader? + switch mode { + case let .peers(_, _, additionalCategories, _, _, _, _): + if !additionalCategories.isEmpty { + let headerType: ChatListSearchItemHeaderType + if case .action = additionalCategories[0].appearance { + // TODO: hack, generalize + headerType = .orImportIntoAnExistingGroup + } else { + headerType = .chats + } + header = ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) + } + default: + break + } + var status: ContactsPeerItemStatus = .none + if isSelecting, let itemPeer = itemPeer { + if displayPresence, let presence = presence { + status = .presence(presence, presentationData.dateTimeFormat) + } else if let (string, multiline, isActive, icon) = statusStringForPeerType(accountPeerId: context.account.peerId, strings: presentationData.strings, peer: itemPeer, isMuted: isRemovedFromTotalUnreadCount, isUnread: combinedReadState?.isUnread ?? false, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: filters, displayAutoremoveTimeout: displayAutoremoveTimeout, autoremoveTimeout: peerEntry.autoremoveTimeout) { + status = .custom(string: string, multiline: multiline, isActive: isActive, icon: icon) + } else { + status = .none + } + } + + let peerContent: ContactsPeerItemPeer + if let threadInfo = threadInfo, let itemPeer = itemPeer { + peerContent = .thread(peer: itemPeer, title: threadInfo.info.title, icon: threadInfo.info.icon, color: threadInfo.info.iconColor) + } else { + peerContent = .peer(peer: itemPeer, chatPeer: chatPeer) + } + + var threadId: Int64? + switch index { + case let .forum(_, _, threadIdValue, _, _): + threadId = threadIdValue + case .chatList: + break + } + + var isForum = false + if let peer = chatPeer, case let .channel(channel) = peer, channel.isForumOrMonoForum { + isForum = true + if editing, case .chatList = mode { + enabled = false + } + } + + var selectable = editing + if case .chatList = mode { + if isForum { + selectable = false + } + } + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem( presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat), sortOrder: presentationData.nameSortOrder, @@ -661,69 +643,37 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL peer: peerContent, status: status, requiresPremiumForMessaging: peerEntry.requiresPremiumForMessaging, - enabled: true, - selection: editing ? .selectable(selected: selected) : .none, + enabled: enabled, + selection: selectable ? .selectable(selected: selected) : .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, - header: nil, + header: header, action: { _ in if let chatPeer = chatPeer { if editing { - nodeInteraction.togglePeerSelected(chatPeer, nil) + nodeInteraction.togglePeerSelected(chatPeer, threadId) } else { - nodeInteraction.peerSelected(chatPeer, nil, nil, nil, false) + nodeInteraction.peerSelected(chatPeer, nil, threadId, nil, false) } } - }, disabledAction: peerEntry.requiresPremiumForMessaging ? { _ in - if let chatPeer { - nodeInteraction.disabledPeerSelected(chatPeer, nil, .premiumRequired) + }, disabledAction: (isForum && editing) && !peerEntry.requiresPremiumForMessaging ? nil : { _ in + if let chatPeer = chatPeer { + nodeInteraction.disabledPeerSelected(chatPeer, threadId, peerEntry.requiresPremiumForMessaging ? .premiumRequired : .generic) } - } : nil, + }, animationCache: nodeInteraction.animationCache, animationRenderer: nodeInteraction.animationRenderer ), directionHint: entry.directionHint) + case .peerType: + let itemPeer = peer.chatOrMonoforumMainPeer + var chatPeer: EnginePeer? + if let peer = peer.peers[peer.peerId] { + chatPeer = peer } - case let .HoleEntry(_, theme): - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(theme: theme), directionHint: entry.directionHint) - case let .GroupReferenceEntry(groupReferenceEntry): - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem( - presentationData: groupReferenceEntry.presentationData, - context: context, - chatListLocation: location, - filterData: filterData, - index: groupReferenceEntry.index, - content: .groupReference(ChatListItemContent.GroupReferenceData( - groupId: groupReferenceEntry.groupId, - peers: groupReferenceEntry.peers, - message: groupReferenceEntry.message, - unreadCount: groupReferenceEntry.unreadCount, - hiddenByDefault: groupReferenceEntry.hiddenByDefault, - appearsPinned: groupReferenceEntry.appearsPinned, - storyState: groupReferenceEntry.storyState.flatMap { storyState in - return ChatListItemContent.StoryState( - stats: storyState.stats, - hasUnseenCloseFriends: storyState.hasUnseenCloseFriends - ) - } - )), - editing: groupReferenceEntry.editing, - hasActiveRevealControls: false, - selected: false, - header: nil, - enabledContextActions: .auto, - hiddenOffset: groupReferenceEntry.hiddenByDefault && !groupReferenceEntry.revealed, - interaction: nodeInteraction - ), directionHint: entry.directionHint) - case let .ContactEntry(contactEntry): - let header: ChatListSearchItemHeader? = nil - var status: ContactsPeerItemStatus = .none - status = .presence(contactEntry.presence, contactEntry.presentationData.dateTimeFormat) - - let presentationData = contactEntry.presentationData - - let peerContent: ContactsPeerItemPeer = .peer(peer: contactEntry.peer, chatPeer: contactEntry.peer) - + let peerContent: ContactsPeerItemPeer = .peer(peer: itemPeer, chatPeer: chatPeer) + let status: ContactsPeerItemStatus = .none + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem( presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat), sortOrder: presentationData.nameSortOrder, @@ -732,31 +682,103 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL peerMode: .generalSearch(isSavedMessages: false), peer: peerContent, status: status, + requiresPremiumForMessaging: peerEntry.requiresPremiumForMessaging, enabled: true, - selection: .none, + selection: editing ? .selectable(selected: selected) : .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, - header: header, + header: nil, action: { _ in - nodeInteraction.peerSelected(contactEntry.peer, nil, nil, nil, false) - }, - disabledAction: nil, + if let chatPeer = chatPeer { + if editing { + nodeInteraction.togglePeerSelected(chatPeer, nil) + } else { + nodeInteraction.peerSelected(chatPeer, nil, nil, nil, false) + } + } + }, disabledAction: peerEntry.requiresPremiumForMessaging ? { _ in + if let chatPeer { + nodeInteraction.disabledPeerSelected(chatPeer, nil, .premiumRequired) + } + } : nil, animationCache: nodeInteraction.animationCache, animationRenderer: nodeInteraction.animationRenderer ), directionHint: entry.directionHint) - case let .ArchiveIntro(presentationData): - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListArchiveInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint) - case let .EmptyIntro(presentationData): - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint) - case let .SectionHeader(presentationData, displayHide): - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSectionHeaderItem(theme: presentationData.theme, strings: presentationData.strings, hide: displayHide ? { - hideChatListContacts(context: context) - } : nil), directionHint: entry.directionHint) + } + case let .HoleEntry(_, theme): + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(theme: theme), directionHint: entry.directionHint) + case let .GroupReferenceEntry(groupReferenceEntry): + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem( + presentationData: groupReferenceEntry.presentationData, + context: context, + chatListLocation: location, + filterData: filterData, + index: groupReferenceEntry.index, + content: .groupReference(ChatListItemContent.GroupReferenceData( + groupId: groupReferenceEntry.groupId, + peers: groupReferenceEntry.peers, + message: groupReferenceEntry.message, + unreadCount: groupReferenceEntry.unreadCount, + hiddenByDefault: groupReferenceEntry.hiddenByDefault, + appearsPinned: groupReferenceEntry.appearsPinned, + storyState: groupReferenceEntry.storyState.flatMap { storyState in + return ChatListItemContent.StoryState( + stats: storyState.stats, + hasUnseenCloseFriends: storyState.hasUnseenCloseFriends + ) + } + )), + editing: groupReferenceEntry.editing, + hasActiveRevealControls: false, + selected: false, + header: nil, + enabledContextActions: .auto, + hiddenOffset: groupReferenceEntry.hiddenByDefault && !groupReferenceEntry.revealed, + interaction: nodeInteraction + ), directionHint: entry.directionHint) + case let .ContactEntry(contactEntry): + let header: ChatListSearchItemHeader? = nil + + var status: ContactsPeerItemStatus = .none + status = .presence(contactEntry.presence, contactEntry.presentationData.dateTimeFormat) + + let presentationData = contactEntry.presentationData + + let peerContent: ContactsPeerItemPeer = .peer(peer: contactEntry.peer, chatPeer: contactEntry.peer) + + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem( + presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat), + sortOrder: presentationData.nameSortOrder, + displayOrder: presentationData.nameDisplayOrder, + context: context, + peerMode: .generalSearch(isSavedMessages: false), + peer: peerContent, + status: status, + enabled: true, + selection: .none, + editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), + index: nil, + header: header, + action: { _ in + nodeInteraction.peerSelected(contactEntry.peer, nil, nil, nil, false) + }, + disabledAction: nil, + animationCache: nodeInteraction.animationCache, + animationRenderer: nodeInteraction.animationRenderer + ), directionHint: entry.directionHint) + case let .ArchiveIntro(presentationData): + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListArchiveInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint) + case let .EmptyIntro(presentationData): + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint) + case let .SectionHeader(presentationData, displayHide): + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSectionHeaderItem(theme: presentationData.theme, strings: presentationData.strings, hide: displayHide ? { + hideChatListContacts(context: context) + } : nil), directionHint: entry.directionHint) } } } -private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, isPremium: Bool, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, entries: [ChatListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { +private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, isPremium: Bool, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, entries: [ChatListNodeViewTransitionUpdateEntry], presentationData: ChatListPresentationData) -> [ListViewUpdateItem] { return entries.map { entry -> ListViewUpdateItem in switch entry.entry { case let .PeerEntry(peerEntry): @@ -828,7 +850,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL hiddenOffset: threadInfo?.isHidden == true && !revealed, interaction: nodeInteraction ), directionHint: entry.directionHint) - case let .peers(filter, isSelecting, _, filters, displayAutoremoveTimeout, displayPresence): + case let .peers(filter, isSelecting, _, _, filters, displayAutoremoveTimeout, displayPresence): let itemPeer = peer.chatOrMonoforumMainPeer var chatPeer: EnginePeer? if let peer = peer.peers[peer.peerId] { @@ -868,7 +890,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL var header: ChatListSearchItemHeader? switch mode { - case let .peers(_, _, additionalCategories, _, _, _): + case let .peers(_, _, additionalCategories, _, _, _, _): if !additionalCategories.isEmpty { let headerType: ChatListSearchItemHeaderType if case .action = additionalCategories[0].appearance { @@ -1066,6 +1088,28 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL } : nil), directionHint: entry.directionHint) case .HeaderEntry: return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyHeaderItem(), directionHint: entry.directionHint) + case let .TopPeer(_, peer): + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem( + presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat), + sortOrder: presentationData.nameSortOrder, + displayOrder: presentationData.nameDisplayOrder, + context: context, + peerMode: .generalSearch(isSavedMessages: false), + peer: .peer(peer: peer, chatPeer: peer), + status: .none, + requiresPremiumForMessaging: false, + enabled: true, + selection: .none, + editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), + index: nil, + header: nil, + action: { _ in + nodeInteraction.peerSelected(peer, nil, nil, nil, false) + }, + disabledAction: nil, + animationCache: nodeInteraction.animationCache, + animationRenderer: nodeInteraction.animationRenderer + ), directionHint: entry.directionHint) case let .AdditionalCategory(index: _, id, title, image, appearance, selected, presentationData): var header: ChatListSearchItemHeader? if case .peerType = mode { @@ -1091,8 +1135,8 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL } } -private func mappedChatListNodeViewListTransition(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, isPremium: Bool, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, transition: ChatListNodeViewTransition) -> ChatListNodeListViewTransition { - return ChatListNodeListViewTransition(chatListView: transition.chatListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, nodeInteraction: nodeInteraction, location: location, isPremium: isPremium, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, entries: transition.insertEntries), updateItems: mappedUpdateEntries(context: context, nodeInteraction: nodeInteraction, location: location, isPremium: isPremium, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, adjustScrollToFirstItem: transition.adjustScrollToFirstItem, animateCrossfade: transition.animateCrossfade) +private func mappedChatListNodeViewListTransition(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, isPremium: Bool, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, presentationData: ChatListPresentationData, transition: ChatListNodeViewTransition) -> ChatListNodeListViewTransition { + return ChatListNodeListViewTransition(chatListView: transition.chatListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, nodeInteraction: nodeInteraction, location: location, isPremium: isPremium, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, entries: transition.insertEntries, presentationData: presentationData), updateItems: mappedUpdateEntries(context: context, nodeInteraction: nodeInteraction, location: location, isPremium: isPremium, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, entries: transition.updateEntries, presentationData: presentationData), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, adjustScrollToFirstItem: transition.adjustScrollToFirstItem, animateCrossfade: transition.animateCrossfade) } private final class ChatListOpaqueTransactionState { @@ -1196,7 +1240,7 @@ public final class ChatListNode: ListViewImpl { return nil case let .PeerId(value): return PeerId(value) - case .ThreadId, .GroupId, .ContactId, .ArchiveIntro, .EmptyIntro, .SectionHeader, .Notice, .additionalCategory: + case .ThreadId, .GroupId, .ContactId, .ArchiveIntro, .EmptyIntro, .SectionHeader, .Notice, .additionalCategory, .TopPeer: return nil } } @@ -1326,7 +1370,7 @@ public final class ChatListNode: ListViewImpl { } var isSelecting = false - if case .peers(_, true, _, _, _, _) = mode { + if case .peers(_, true, _, _, _, _, _) = mode { isSelecting = true } @@ -1839,7 +1883,7 @@ public final class ChatListNode: ListViewImpl { let currentRemovingItemId = self.currentRemovingItemId let savedMessagesPeer: Signal - if case let .peers(filter, _, _, _, _, _) = mode, filter.contains(.onlyWriteable), case .chatList = location, self.chatListFilter == nil { + if case let .peers(filter, _, _, _, _, _, _) = mode, filter.contains(.onlyWriteable), case .chatList = location, self.chatListFilter == nil { savedMessagesPeer = context.account.postbox.loadedPeerWithId(context.account.peerId) |> map(Optional.init) |> map { peer in @@ -2102,7 +2146,7 @@ public final class ChatListNode: ListViewImpl { case .chatList: isEmpty = false return true - case let .peers(filter, _, _, _, _, _): + case let .peers(filter, _, _, _, _, _, _): guard !filter.contains(.excludeSavedMessages) || peer.peerId != currentPeerId else { return false } guard !filter.contains(.excludeSavedMessages) || !peer.peerId.isReplies else { return false } guard !filter.contains(.excludeSecretChats) || peer.peerId.namespace != Namespaces.Peer.SecretChat else { return false } @@ -2538,9 +2582,10 @@ public final class ChatListNode: ListViewImpl { if accountIsPremium != previousAccountIsPremium.swap(accountIsPremium) { forceAllUpdated = true } + let presentationData = state.presentationData return preparedChatListNodeViewTransition(from: previousView, to: processedView, reason: reason, previewing: previewing, disableAnimations: disableAnimations, account: context.account, scrollPosition: updatedScrollPosition, searchMode: searchMode, forceAllUpdated: forceAllUpdated) - |> map({ mappedChatListNodeViewListTransition(context: context, nodeInteraction: nodeInteraction, location: location, isPremium: accountIsPremium, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, transition: $0) }) + |> map({ mappedChatListNodeViewListTransition(context: context, nodeInteraction: nodeInteraction, location: location, isPremium: accountIsPremium, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, presentationData: presentationData, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : viewProcessingQueue) } @@ -3391,7 +3436,7 @@ public final class ChatListNode: ListViewImpl { } else { break loop } - case .ArchiveIntro, .EmptyIntro, .SectionHeader, .HeaderEntry, .AdditionalCategory: + case .ArchiveIntro, .EmptyIntro, .SectionHeader, .HeaderEntry, .AdditionalCategory, .TopPeer: break } } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index 879c0daeb8..d98fddcd84 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -18,6 +18,7 @@ enum ChatListNodeEntryId: Hashable { case SectionHeader case Notice case additionalCategory(Int) + case TopPeer(EnginePeer.Id) } enum ChatListNodeEntrySortIndex: Comparable { @@ -25,6 +26,7 @@ enum ChatListNodeEntrySortIndex: Comparable { case additionalCategory(Int) case sectionHeader case contact(id: EnginePeer.Id, presence: EnginePeer.Presence) + case topPeer(Int) static func <(lhs: ChatListNodeEntrySortIndex, rhs: ChatListNodeEntrySortIndex) -> Bool { switch lhs { @@ -34,6 +36,8 @@ enum ChatListNodeEntrySortIndex: Comparable { return lhsIndex < rhsIndex case .additionalCategory: return false + case .topPeer: + return false case .sectionHeader: return true case .contact: @@ -43,6 +47,8 @@ enum ChatListNodeEntrySortIndex: Comparable { switch rhs { case let .additionalCategory(rhsIndex): return lhsIndex < rhsIndex + case .topPeer: + return true case .index: return true case .sectionHeader: @@ -50,9 +56,18 @@ enum ChatListNodeEntrySortIndex: Comparable { case .contact: return true } + case let .topPeer(lhsIndex): + switch rhs { + case .additionalCategory: + return false + case let .topPeer(rhsIndex): + return lhsIndex < rhsIndex + case .index, .sectionHeader, .contact: + return true + } case .sectionHeader: switch rhs { - case .additionalCategory, .index, .sectionHeader: + case .additionalCategory, .topPeer, .index, .sectionHeader: return false case .contact: return true @@ -405,6 +420,7 @@ enum ChatListNodeEntry: Comparable, Identifiable { case EmptyIntro(presentationData: ChatListPresentationData) case SectionHeader(presentationData: ChatListPresentationData, displayHide: Bool) case AdditionalCategory(index: Int, id: Int, title: String, image: UIImage?, appearance: ChatListNodeAdditionalCategory.Appearance, selected: Bool, presentationData: ChatListPresentationData) + case TopPeer(index: Int, peer: EnginePeer) var sortIndex: ChatListNodeEntrySortIndex { switch self { @@ -426,6 +442,8 @@ enum ChatListNodeEntry: Comparable, Identifiable { return .sectionHeader case let .AdditionalCategory(index, _, _, _, _, _, _): return .additionalCategory(index) + case let .TopPeer(index, _): + return .topPeer(index) } } @@ -454,6 +472,8 @@ enum ChatListNodeEntry: Comparable, Identifiable { return .SectionHeader case let .AdditionalCategory(_, id, _, _, _, _, _): return .additionalCategory(id) + case let .TopPeer(_, peer): + return .TopPeer(peer.id) } } @@ -551,6 +571,12 @@ enum ChatListNodeEntry: Comparable, Identifiable { } else { return false } + case let .TopPeer(index, peer): + if case .TopPeer(index, peer) = rhs { + return true + } else { + return false + } } } } @@ -930,12 +956,18 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState, } if !view.hasLater { - if case let .peers(_, _, additionalCategories, _, _, _) = mode { + if case let .peers(_, _, additionalCategories, topPeers, _, _, _) = mode { var index = 0 for category in additionalCategories.reversed() { result.append(.AdditionalCategory(index: index, id: category.id, title: category.title, image: category.icon, appearance: category.appearance, selected: state.selectedAdditionalCategoryIds.contains(category.id), presentationData: state.presentationData)) index += 1 } + + index = 0 + for topPeer in topPeers { + result.append(.TopPeer(index: index, peer: topPeer)) + index += 1 + } } else if case let .peerType(types, hasCreate) = mode, !result.isEmpty && hasCreate { for type in types { switch type { diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift index 6d5c9bb67a..860ba6ad33 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift @@ -25,6 +25,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon private let filter: ChatListNodePeersFilter private let forumPeerId: (id: EnginePeer.Id, isMonoforum: Bool)? private let selectForumThreads: Bool + private let suggestedPeers: [EnginePeer] private let attemptSelection: ((EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void)? private let createNewGroup: (() -> Void)? @@ -114,6 +115,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon self.immediatelySwitchToContacts = params.immediatelySwitchToContacts self.immediatelyActivateMultipleSelection = params.immediatelyActivateMultipleSelection self.multipleSelectionLimit = params.multipleSelectionLimit + self.suggestedPeers = params.suggestedPeers super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData, style: .glass)) @@ -263,7 +265,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon override public func loadDisplayNode() { self.navigationBar?.secondaryContentHeight = 44.0 + 10.0 - self.displayNode = PeerSelectionControllerNode(context: self.context, controller: self, presentationData: self.presentationData, filter: self.filter, forumPeerId: self.forumPeerId, hasFilters: self.hasFilters, hasChatListSelector: self.hasChatListSelector, hasContactSelector: self.hasContactSelector, hasGlobalSearch: self.hasGlobalSearch, forwardedMessageIds: self.forwardedMessageIds, hasTypeHeaders: self.hasTypeHeaders, requestPeerType: self.requestPeerType, hasCreation: self.hasCreation, createNewGroup: self.createNewGroup, present: { [weak self] c, a in + self.displayNode = PeerSelectionControllerNode(context: self.context, controller: self, presentationData: self.presentationData, filter: self.filter, forumPeerId: self.forumPeerId, hasFilters: self.hasFilters, hasChatListSelector: self.hasChatListSelector, hasContactSelector: self.hasContactSelector, hasGlobalSearch: self.hasGlobalSearch, forwardedMessageIds: self.forwardedMessageIds, hasTypeHeaders: self.hasTypeHeaders, requestPeerType: self.requestPeerType, hasCreation: self.hasCreation, createNewGroup: self.createNewGroup, suggestedPeers: self.suggestedPeers, present: { [weak self] c, a in self?.present(c, in: .window(.root), with: a) }, presentInGlobalOverlay: { [weak self] c, a in self?.presentInGlobalOverlay(c, with: a) diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index b438e2e511..abc5f036a2 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -41,6 +41,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { private let forwardedMessageIds: [EngineMessage.Id] private let hasTypeHeaders: Bool private let requestPeerType: [ReplyMarkupButtonRequestPeerType]? + private let suggestedPeers: [EnginePeer] private var presentationInterfaceState: ChatPresentationInterfaceState private let presentationInterfaceStatePromise = ValuePromise() @@ -115,7 +116,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { return (self.presentationData, self.presentationDataPromise.get()) } - init(context: AccountContext, controller: PeerSelectionControllerImpl, presentationData: PresentationData, filter: ChatListNodePeersFilter, forumPeerId: (id: EnginePeer.Id, isMonoforum: Bool)?, hasFilters: Bool, hasChatListSelector: Bool, hasContactSelector: Bool, hasGlobalSearch: Bool, forwardedMessageIds: [EngineMessage.Id], hasTypeHeaders: Bool, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, hasCreation: Bool, createNewGroup: (() -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) { + init(context: AccountContext, controller: PeerSelectionControllerImpl, presentationData: PresentationData, filter: ChatListNodePeersFilter, forumPeerId: (id: EnginePeer.Id, isMonoforum: Bool)?, hasFilters: Bool, hasChatListSelector: Bool, hasContactSelector: Bool, hasGlobalSearch: Bool, forwardedMessageIds: [EngineMessage.Id], hasTypeHeaders: Bool, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, hasCreation: Bool, createNewGroup: (() -> Void)?, suggestedPeers: [EnginePeer], present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) { self.context = context self.controller = controller self.present = present @@ -127,6 +128,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.forwardedMessageIds = forwardedMessageIds self.hasTypeHeaders = hasTypeHeaders self.requestPeerType = requestPeerType + self.suggestedPeers = suggestedPeers self.presentationData = presentationData @@ -209,7 +211,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { if let requestPeerType = self.requestPeerType { chatListMode = .peerType(type: requestPeerType, hasCreate: hasCreation) } else { - chatListMode = .peers(filter: filter, isSelecting: false, additionalCategories: chatListCategories, chatListFilters: nil, displayAutoremoveTimeout: false, displayPresence: false) + chatListMode = .peers(filter: filter, isSelecting: false, additionalCategories: chatListCategories, topPeers: self.suggestedPeers, chatListFilters: nil, displayAutoremoveTimeout: false, displayPresence: false) } if hasFilters { diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/BUILD b/submodules/TelegramUI/Components/StorageUsageScreen/BUILD index d935fec442..137ec4fb05 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/BUILD +++ b/submodules/TelegramUI/Components/StorageUsageScreen/BUILD @@ -46,6 +46,8 @@ swift_library( "//submodules/SegmentedControlNode", "//submodules/TelegramUIPreferences", "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/TelegramUI/Components/HorizontalTabsComponent", + "//submodules/TelegramUI/Components/LiquidLens", "//submodules/TelegramUI/Components/EdgeEffect", ], visibility = [ diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift index 1b80afc5b6..89a1494604 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift @@ -704,43 +704,46 @@ final class StorageFileListPanelComponent: Component { private struct ItemLayout: Equatable { let containerInsets: UIEdgeInsets let containerWidth: CGFloat + let sideInset: CGFloat let itemHeight: CGFloat let itemCount: Int - + let contentHeight: CGFloat - + init( containerInsets: UIEdgeInsets, containerWidth: CGFloat, + sideInset: CGFloat, itemHeight: CGFloat, itemCount: Int ) { self.containerInsets = containerInsets self.containerWidth = containerWidth + self.sideInset = sideInset self.itemHeight = itemHeight self.itemCount = itemCount - + self.contentHeight = containerInsets.top + containerInsets.bottom + CGFloat(itemCount) * itemHeight } - + func visibleItems(for rect: CGRect) -> Range? { let offsetRect = rect.offsetBy(dx: -self.containerInsets.left, dy: -self.containerInsets.top) var minVisibleRow = Int(floor((offsetRect.minY) / (self.itemHeight))) minVisibleRow = max(0, minVisibleRow) let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemHeight))) - + let minVisibleIndex = minVisibleRow let maxVisibleIndex = maxVisibleRow - + if maxVisibleIndex >= minVisibleIndex { return minVisibleIndex ..< (maxVisibleIndex + 1) } else { return nil } } - + func itemFrame(for index: Int) -> CGRect { - return CGRect(origin: CGPoint(x: 0.0, y: self.containerInsets.top + CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerWidth, height: self.itemHeight)) + return CGRect(origin: CGPoint(x: self.sideInset, y: self.containerInsets.top + CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerWidth - self.sideInset * 2.0, height: self.itemHeight)) } } @@ -752,37 +755,48 @@ final class StorageFileListPanelComponent: Component { class View: UIView, UIScrollViewDelegate { private let scrollView: ScrollViewImpl - + private let listBackgroundView: UIImageView + private let listMaskView: UIImageView + private let measureItem = ComponentView() private var visibleItems: [EngineMessage.Id: ComponentView] = [:] - + private var ignoreScrolling: Bool = false - + private var component: StorageFileListPanelComponent? private var environment: StorageUsagePanelEnvironment? private var itemLayout: ItemLayout? - + override init(frame: CGRect) { self.scrollView = ScrollViewImpl() - + self.listBackgroundView = UIImageView() + self.listBackgroundView.image = generateStretchableFilledCircleImage(diameter: 26.0 * 2.0, color: .white)?.withRenderingMode(.alwaysTemplate) + self.listMaskView = UIImageView() + self.listMaskView.image = generateImage(CGSize(width: 16.0 + 26.0 * 2.0 + 16.0, height: 26.0 * 2.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.white.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 16.0, y: 0.0), size: CGSize(width: 26.0 * 2.0, height: 26.0 * 2.0))) + })?.stretchableImage(withLeftCapWidth: 16 + 26, topCapHeight: 26).withRenderingMode(.alwaysTemplate) + super.init(frame: frame) - + + self.addSubview(self.listBackgroundView) + self.scrollView.delaysContentTouches = true self.scrollView.canCancelContentTouches = true - self.scrollView.clipsToBounds = false - if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { - self.scrollView.contentInsetAdjustmentBehavior = .never - } - if #available(iOS 13.0, *) { - self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false - } - self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.contentInsetAdjustmentBehavior = .never + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + self.scrollView.showsVerticalScrollIndicator = false self.scrollView.showsHorizontalScrollIndicator = false self.scrollView.alwaysBounceHorizontal = false self.scrollView.scrollsToTop = false self.scrollView.delegate = self self.scrollView.clipsToBounds = true self.addSubview(self.scrollView) + self.addSubview(self.listMaskView) } required init?(coder: NSCoder) { @@ -792,13 +806,44 @@ final class StorageFileListPanelComponent: Component { func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { self.updateScrolling(transition: .immediate) + self.updateListBackground(transition: .immediate) } } - + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { cancelContextGestures(view: scrollView) } - + + private func updateListBackground(transition: ComponentTransition) { + guard let itemLayout = self.itemLayout else { + return + } + let size = CGSize(width: itemLayout.containerWidth, height: self.scrollView.bounds.height) + guard size.width != 0.0 else { + return + } + + let topInset = itemLayout.containerInsets.top + + var distanceToTop: CGFloat = -100.0 + var distanceToBottom: CGFloat = -100.0 + + let scrollingOffset = self.scrollView.contentOffset.y + distanceToTop = -scrollingOffset + topInset + + let itemsContentHeight = self.scrollView.contentSize.height - itemLayout.containerInsets.bottom + let contentBottomOffset = itemsContentHeight - scrollingOffset - self.scrollView.bounds.height + distanceToBottom = -contentBottomOffset + + distanceToTop = max(-100.0, distanceToTop) + distanceToBottom = max(-100.0, distanceToBottom) + + let listBackgroundFrame = CGRect(origin: CGPoint(x: 16.0, y: distanceToTop), size: CGSize(width: max(1.0, size.width - 16.0 * 2.0), height: max(1.0, size.height - distanceToBottom - distanceToTop))) + let listMaskFrame = CGRect(origin: CGPoint(x: 0.0, y: listBackgroundFrame.minY), size: CGSize(width: listBackgroundFrame.width + 16.0 * 2.0, height: listBackgroundFrame.height)) + transition.setFrame(view: self.listBackgroundView, frame: listBackgroundFrame) + transition.setFrame(view: self.listMaskView, frame: listMaskFrame) + } + private func updateScrolling(transition: ComponentTransition) { guard let component = self.component, let environment = self.environment, let items = component.items, let itemLayout = self.itemLayout else { return @@ -975,7 +1020,7 @@ final class StorageFileListPanelComponent: Component { contextAction: component.contextAction )), environment: {}, - containerSize: CGSize(width: itemLayout.containerWidth, height: itemLayout.itemHeight) + containerSize: CGSize(width: itemLayout.containerWidth - itemLayout.sideInset * 2.0, height: itemLayout.itemHeight) ) let itemFrame = itemLayout.itemFrame(for: index) if let itemComponentView = itemView.view { @@ -1008,6 +1053,9 @@ final class StorageFileListPanelComponent: Component { let environment = environment[StorageUsagePanelEnvironment.self].value self.environment = environment + + self.listBackgroundView.tintColor = environment.theme.list.itemBlocksBackgroundColor + self.listMaskView.tintColor = environment.theme.list.blocksBackgroundColor let measureItemSize = self.measureItem.update( transition: .immediate, @@ -1034,6 +1082,7 @@ final class StorageFileListPanelComponent: Component { let itemLayout = ItemLayout( containerInsets: environment.containerInsets, containerWidth: availableSize.width, + sideInset: 16.0, itemHeight: measureItemSize.height, itemCount: component.items?.items.count ?? 0 ) @@ -1060,7 +1109,8 @@ final class StorageFileListPanelComponent: Component { } self.ignoreScrolling = false self.updateScrolling(transition: transition) - + self.updateListBackground(transition: transition) + return availableSize } } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerListPanelComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerListPanelComponent.swift index 818ac08da7..08200ed5ad 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerListPanelComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerListPanelComponent.swift @@ -454,43 +454,46 @@ final class StoragePeerListPanelComponent: Component { private struct ItemLayout: Equatable { let containerInsets: UIEdgeInsets let containerWidth: CGFloat + let sideInset: CGFloat let itemHeight: CGFloat let itemCount: Int - + let contentHeight: CGFloat - + init( containerInsets: UIEdgeInsets, containerWidth: CGFloat, + sideInset: CGFloat, itemHeight: CGFloat, itemCount: Int ) { self.containerInsets = containerInsets self.containerWidth = containerWidth + self.sideInset = sideInset self.itemHeight = itemHeight self.itemCount = itemCount - + self.contentHeight = containerInsets.top + containerInsets.bottom + CGFloat(itemCount) * itemHeight } - + func visibleItems(for rect: CGRect) -> Range? { let offsetRect = rect.offsetBy(dx: -self.containerInsets.left, dy: -self.containerInsets.top) var minVisibleRow = Int(floor((offsetRect.minY) / (self.itemHeight))) minVisibleRow = max(0, minVisibleRow) let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemHeight))) - + let minVisibleIndex = minVisibleRow let maxVisibleIndex = maxVisibleRow - + if maxVisibleIndex >= minVisibleIndex { return minVisibleIndex ..< (maxVisibleIndex + 1) } else { return nil } } - + func itemFrame(for index: Int) -> CGRect { - return CGRect(origin: CGPoint(x: 0.0, y: self.containerInsets.top + CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerWidth, height: self.itemHeight)) + return CGRect(origin: CGPoint(x: self.sideInset, y: self.containerInsets.top + CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerWidth - self.sideInset * 2.0, height: self.itemHeight)) } } @@ -502,7 +505,9 @@ final class StoragePeerListPanelComponent: Component { class View: UIView, UIScrollViewDelegate { private let scrollView: ScrollViewImpl - + private let listBackgroundView: UIImageView + private let listMaskView: UIImageView + private let measureItem = ComponentView() private var visibleItems: [EnginePeer.Id: ComponentView] = [:] @@ -514,25 +519,35 @@ final class StoragePeerListPanelComponent: Component { override init(frame: CGRect) { self.scrollView = ScrollViewImpl() - + + self.listBackgroundView = UIImageView() + self.listBackgroundView.image = generateStretchableFilledCircleImage(diameter: 26.0 * 2.0, color: .white)?.withRenderingMode(.alwaysTemplate) + self.listMaskView = UIImageView() + self.listMaskView.image = generateImage(CGSize(width: 16.0 + 26.0 * 2.0 + 16.0, height: 26.0 * 2.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.white.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 16.0, y: 0.0), size: CGSize(width: 26.0 * 2.0, height: 26.0 * 2.0))) + })?.stretchableImage(withLeftCapWidth: 16 + 26, topCapHeight: 26).withRenderingMode(.alwaysTemplate) + super.init(frame: frame) - + + self.addSubview(self.listBackgroundView) + self.scrollView.delaysContentTouches = true self.scrollView.canCancelContentTouches = true - self.scrollView.clipsToBounds = false - if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { - self.scrollView.contentInsetAdjustmentBehavior = .never - } - if #available(iOS 13.0, *) { - self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false - } - self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.contentInsetAdjustmentBehavior = .never + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + self.scrollView.showsVerticalScrollIndicator = false self.scrollView.showsHorizontalScrollIndicator = false self.scrollView.alwaysBounceHorizontal = false self.scrollView.scrollsToTop = false self.scrollView.delegate = self self.scrollView.clipsToBounds = true self.addSubview(self.scrollView) + self.addSubview(self.listMaskView) } required init?(coder: NSCoder) { @@ -542,13 +557,44 @@ final class StoragePeerListPanelComponent: Component { func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { self.updateScrolling(transition: .immediate) + self.updateListBackground(transition: .immediate) } } func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { cancelContextGestures(view: scrollView) } - + + private func updateListBackground(transition: ComponentTransition) { + guard let itemLayout = self.itemLayout else { + return + } + let size = CGSize(width: itemLayout.containerWidth, height: self.scrollView.bounds.height) + guard size.width != 0.0 else { + return + } + + let topInset = itemLayout.containerInsets.top + + var distanceToTop: CGFloat = -100.0 + var distanceToBottom: CGFloat = -100.0 + + let scrollingOffset = self.scrollView.contentOffset.y + distanceToTop = -scrollingOffset + topInset + + let itemsContentHeight = self.scrollView.contentSize.height - itemLayout.containerInsets.bottom + let contentBottomOffset = itemsContentHeight - scrollingOffset - self.scrollView.bounds.height + distanceToBottom = -contentBottomOffset + + distanceToTop = max(-100.0, distanceToTop) + distanceToBottom = max(-100.0, distanceToBottom) + + let listBackgroundFrame = CGRect(origin: CGPoint(x: 16.0, y: distanceToTop), size: CGSize(width: max(1.0, size.width - 16.0 * 2.0), height: max(1.0, size.height - distanceToBottom - distanceToTop))) + let listMaskFrame = CGRect(origin: CGPoint(x: 0.0, y: listBackgroundFrame.minY), size: CGSize(width: listBackgroundFrame.width + 16.0 * 2.0, height: listBackgroundFrame.height)) + transition.setFrame(view: self.listBackgroundView, frame: listBackgroundFrame) + transition.setFrame(view: self.listMaskView, frame: listMaskFrame) + } + private func updateScrolling(transition: ComponentTransition) { guard let component = self.component, let environment = self.environment, let items = component.items, let itemLayout = self.itemLayout else { return @@ -607,7 +653,7 @@ final class StoragePeerListPanelComponent: Component { contextAction: component.contextAction )), environment: {}, - containerSize: CGSize(width: itemLayout.containerWidth, height: itemLayout.itemHeight) + containerSize: CGSize(width: itemLayout.containerWidth - itemLayout.sideInset * 2.0, height: itemLayout.itemHeight) ) let itemFrame = itemLayout.itemFrame(for: index) if let itemComponentView = itemView.view { @@ -640,6 +686,9 @@ final class StoragePeerListPanelComponent: Component { let environment = environment[StorageUsagePanelEnvironment.self].value self.environment = environment + + self.listBackgroundView.tintColor = environment.theme.list.itemBlocksBackgroundColor + self.listMaskView.tintColor = environment.theme.list.blocksBackgroundColor let measureItemSize = self.measureItem.update( transition: .immediate, @@ -664,6 +713,7 @@ final class StoragePeerListPanelComponent: Component { let itemLayout = ItemLayout( containerInsets: environment.containerInsets, containerWidth: availableSize.width, + sideInset: 16.0, itemHeight: measureItemSize.height, itemCount: component.items?.items.count ?? 0 ) @@ -690,7 +740,8 @@ final class StoragePeerListPanelComponent: Component { } self.ignoreScrolling = false self.updateScrolling(transition: transition) - + self.updateListBackground(transition: transition) + return availableSize } } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift index 57576d65b8..1c452ecd59 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift @@ -4,6 +4,8 @@ import Display import ComponentFlow import ComponentDisplayAdapters import TelegramPresentationData +import HorizontalTabsComponent +import GlassBackgroundComponent final class StorageUsagePanelContainerEnvironment: Equatable { let isScrollable: Bool @@ -63,264 +65,6 @@ final class StorageUsagePanelEnvironment: Equatable { } } -private final class StorageUsageHeaderItemComponent: CombinedComponent { - let theme: PresentationTheme - let title: String - let activityFraction: CGFloat - - init( - theme: PresentationTheme, - title: String, - activityFraction: CGFloat - ) { - self.theme = theme - self.title = title - self.activityFraction = activityFraction - } - - static func ==(lhs: StorageUsageHeaderItemComponent, rhs: StorageUsageHeaderItemComponent) -> Bool { - if lhs.theme !== rhs.theme { - return false - } - if lhs.title != rhs.title { - return false - } - if lhs.activityFraction != rhs.activityFraction { - return false - } - return true - } - - static var body: Body { - let activeText = Child(Text.self) - let inactiveText = Child(Text.self) - - return { context in - let activeText = activeText.update( - component: Text(text: context.component.title, font: Font.medium(14.0), color: context.component.theme.list.itemAccentColor), - availableSize: context.availableSize, - transition: .immediate - ) - let inactiveText = inactiveText.update( - component: Text(text: context.component.title, font: Font.medium(14.0), color: context.component.theme.list.itemSecondaryTextColor), - availableSize: context.availableSize, - transition: .immediate - ) - - context.add(activeText - .position(CGPoint(x: activeText.size.width * 0.5, y: activeText.size.height * 0.5)) - .opacity(context.component.activityFraction) - ) - context.add(inactiveText - .position(CGPoint(x: inactiveText.size.width * 0.5, y: inactiveText.size.height * 0.5)) - .opacity(1.0 - context.component.activityFraction) - ) - - return activeText.size - } - } -} - -private final class StorageUsageHeaderComponent: Component { - struct Item: Equatable { - let id: AnyHashable - let title: String - - init( - id: AnyHashable, - title: String - ) { - self.id = id - self.title = title - } - } - - let theme: PresentationTheme - let items: [Item] - let activeIndex: Int - let transitionFraction: CGFloat - let switchToPanel: (AnyHashable) -> Void - - init( - theme: PresentationTheme, - items: [Item], - activeIndex: Int, - transitionFraction: CGFloat, - switchToPanel: @escaping (AnyHashable) -> Void - ) { - self.theme = theme - self.items = items - self.activeIndex = activeIndex - self.transitionFraction = transitionFraction - self.switchToPanel = switchToPanel - } - - static func ==(lhs: StorageUsageHeaderComponent, rhs: StorageUsageHeaderComponent) -> Bool { - if lhs.theme !== rhs.theme { - return false - } - if lhs.items != rhs.items { - return false - } - if lhs.activeIndex != rhs.activeIndex { - return false - } - if lhs.transitionFraction != rhs.transitionFraction { - return false - } - return true - } - - class View: UIView { - private var component: StorageUsageHeaderComponent? - - private var visibleItems: [AnyHashable: ComponentView] = [:] - private let activeItemLayer: SimpleLayer - - override init(frame: CGRect) { - self.activeItemLayer = SimpleLayer() - self.activeItemLayer.cornerRadius = 2.0 - self.activeItemLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - - super.init(frame: frame) - - self.layer.addSublayer(self.activeItemLayer) - - self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - let point = recognizer.location(in: self) - var closestId: (CGFloat, AnyHashable)? - if self.bounds.contains(point) { - for (id, item) in self.visibleItems { - if let itemView = item.view { - let distance: CGFloat = min(abs(point.x - itemView.frame.minX), abs(point.x - itemView.frame.maxX)) - if let closestIdValue = closestId { - if distance < closestIdValue.0 { - closestId = (distance, id) - } - } else { - closestId = (distance, id) - } - } - } - } - if let closestId = closestId, let component = self.component { - component.switchToPanel(closestId.1) - } - } - } - - func update(component: StorageUsageHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - let themeUpdated = self.component?.theme !== component.theme - - self.component = component - - var validIds = Set() - for i in 0 ..< component.items.count { - let item = component.items[i] - validIds.insert(item.id) - - let itemView: ComponentView - var itemTransition = transition - if let current = self.visibleItems[item.id] { - itemView = current - } else { - itemTransition = .immediate - itemView = ComponentView() - self.visibleItems[item.id] = itemView - } - - let activeIndex: CGFloat = CGFloat(component.activeIndex) - component.transitionFraction - let activityDistance: CGFloat = abs(activeIndex - CGFloat(i)) - - let activityFraction: CGFloat - if activityDistance < 1.0 { - activityFraction = 1.0 - activityDistance - } else { - activityFraction = 0.0 - } - - let itemSize = itemView.update( - transition: itemTransition, - component: AnyComponent(StorageUsageHeaderItemComponent( - theme: component.theme, - title: item.title, - activityFraction: activityFraction - )), - environment: {}, - containerSize: availableSize - ) - - let itemHorizontalSpace = availableSize.width / CGFloat(component.items.count) - let itemX: CGFloat - if component.items.count == 1 { - itemX = 37.0 - } else { - itemX = itemHorizontalSpace * CGFloat(i) + floor((itemHorizontalSpace - itemSize.width) / 2.0) - } - - let itemFrame = CGRect(origin: CGPoint(x: itemX, y: floor((availableSize.height - itemSize.height) / 2.0)), size: itemSize) - if let itemComponentView = itemView.view { - if itemComponentView.superview == nil { - self.addSubview(itemComponentView) - itemComponentView.isUserInteractionEnabled = false - } - itemTransition.setFrame(view: itemComponentView, frame: itemFrame) - } - } - - if component.activeIndex < component.items.count { - let activeView = self.visibleItems[component.items[component.activeIndex].id]?.view - let nextIndex: Int - if component.transitionFraction > 0.0 { - nextIndex = max(0, component.activeIndex - 1) - } else { - nextIndex = min(component.items.count - 1, component.activeIndex + 1) - } - let nextView = self.visibleItems[component.items[nextIndex].id]?.view - if let activeView = activeView, let nextView = nextView { - let mergedFrame = activeView.frame.interpolate(to: nextView.frame, amount: abs(component.transitionFraction)) - transition.setFrame(layer: self.activeItemLayer, frame: CGRect(origin: CGPoint(x: mergedFrame.minX, y: availableSize.height - 3.0), size: CGSize(width: mergedFrame.width, height: 3.0))) - } - } - - if themeUpdated { - self.activeItemLayer.backgroundColor = component.theme.list.itemAccentColor.cgColor - } - - var removeIds: [AnyHashable] = [] - for (id, itemView) in self.visibleItems { - if !validIds.contains(id) { - removeIds.append(id) - if let itemComponentView = itemView.view { - itemComponentView.removeFromSuperview() - } - } - } - for id in removeIds { - self.visibleItems.removeValue(forKey: id) - } - - return availableSize - } - } - - 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) - } -} - final class StorageUsagePanelContainerComponent: Component { typealias EnvironmentType = StorageUsagePanelContainerEnvironment @@ -383,10 +127,9 @@ final class StorageUsagePanelContainerComponent: Component { } class View: UIView, UIGestureRecognizerDelegate { - private let topPanelBackgroundView: UIView - private let topPanelMergedBackgroundView: UIView - private let topPanelSeparatorLayer: SimpleLayer - private let header = ComponentView() + private let tabsBackgroundContainer: GlassBackgroundContainerView + private let tabsBackgroundView: GlassBackgroundView + private let tabsContainer = ComponentView() private var component: StorageUsagePanelContainerComponent? private weak var state: EmptyComponentState? @@ -396,25 +139,21 @@ final class StorageUsagePanelContainerComponent: Component { private var actualVisibleIds = Set() private var currentId: AnyHashable? private var transitionFraction: CGFloat = 0.0 + private var isDraggingTabs: Bool = false private var animatingTransition: Bool = false override init(frame: CGRect) { - self.topPanelBackgroundView = UIView() - - self.topPanelMergedBackgroundView = UIView() - self.topPanelMergedBackgroundView.alpha = 0.0 - - self.topPanelSeparatorLayer = SimpleLayer() - + self.tabsBackgroundContainer = GlassBackgroundContainerView() + self.tabsBackgroundView = GlassBackgroundView() + self.panelsBackgroundLayer = SimpleLayer() - + super.init(frame: frame) - + self.layer.addSublayer(self.panelsBackgroundLayer) - self.addSubview(self.topPanelBackgroundView) - self.addSubview(self.topPanelMergedBackgroundView) - self.layer.addSublayer(self.topPanelSeparatorLayer) - + self.tabsBackgroundContainer.contentView.addSubview(self.tabsBackgroundView) + self.addSubview(self.tabsBackgroundContainer) + let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in guard let self, let component = self.component, let currentId = self.currentId else { return [] @@ -422,11 +161,7 @@ final class StorageUsagePanelContainerComponent: Component { guard let index = component.items.firstIndex(where: { $0.id == currentId }) else { return [] } - - /*if strongSelf.tabsContainerNode.bounds.contains(strongSelf.view.convert(point, to: strongSelf.tabsContainerNode.view)) { - return [] - }*/ - + if index == 0 { return .left } @@ -480,7 +215,8 @@ final class StorageUsagePanelContainerComponent: Component { } cancelContextGestures(view: self) - + self.isDraggingTabs = true + //self.animatingTransition = true case .changed: guard let component = self.component, let currentId = self.currentId else { @@ -535,6 +271,7 @@ final class StorageUsagePanelContainerComponent: Component { component.currentPanelUpdated(currentId, transition) } + self.isDraggingTabs = false self.animatingTransition = false //self.currentPaneUpdated?(false) @@ -545,8 +282,16 @@ final class StorageUsagePanelContainerComponent: Component { } func updateNavigationMergeFactor(value: CGFloat, transition: ComponentTransition) { - transition.setAlpha(view: self.topPanelMergedBackgroundView, alpha: value) - transition.setAlpha(view: self.topPanelBackgroundView, alpha: 1.0 - value) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let component = self.component else { + return nil + } + if point.y < component.insets.top { + return nil + } + return super.hitTest(point, with: event) } func update(component: StorageUsagePanelContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { @@ -558,34 +303,30 @@ final class StorageUsagePanelContainerComponent: Component { self.state = state if themeUpdated { - self.panelsBackgroundLayer.backgroundColor = component.theme.list.itemBlocksBackgroundColor.cgColor - self.topPanelSeparatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor - self.topPanelBackgroundView.backgroundColor = component.theme.list.itemBlocksBackgroundColor - self.topPanelMergedBackgroundView.backgroundColor = component.theme.list.blocksBackgroundColor + //self.panelsBackgroundLayer.backgroundColor = component.theme.list.itemBlocksBackgroundColor.cgColor } - let topPanelCoverHeight: CGFloat = 10.0 - - let topPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: -topPanelCoverHeight), size: CGSize(width: availableSize.width, height: 44.0)) - transition.setFrame(view: self.topPanelBackgroundView, frame: topPanelFrame) - transition.setFrame(view: self.topPanelMergedBackgroundView, frame: topPanelFrame) - - transition.setFrame(layer: self.panelsBackgroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelFrame.maxY))) - - transition.setFrame(layer: self.topPanelSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: UIScreenPixel))) - + let tabsHeight: CGFloat = 40.0 + let tabsTopInset: CGFloat = component.insets.top + 10.0 + let tabsBottomInset: CGFloat = 10.0 + let tabsSideInset: CGFloat = 16.0 + component.insets.left + + let tabsContainerSize = CGSize(width: availableSize.width - tabsSideInset * 2.0, height: tabsHeight) + + transition.setFrame(layer: self.panelsBackgroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: component.insets.top), size: CGSize(width: availableSize.width, height: availableSize.height - component.insets.top))) + if let currentIdValue = self.currentId, !component.items.contains(where: { $0.id == currentIdValue }) { self.currentId = nil } if self.currentId == nil { self.currentId = component.items.first?.id } - + var visibleIds = Set() var currentIndex: Int? if let currentId = self.currentId { visibleIds.insert(currentId) - + if let index = component.items.firstIndex(where: { $0.id == currentId }) { currentIndex = index if index != 0 { @@ -596,50 +337,73 @@ final class StorageUsagePanelContainerComponent: Component { } } } - - let _ = self.header.update( + + let tabsContainerEffectiveSize = self.tabsContainer.update( transition: transition, - component: AnyComponent(StorageUsageHeaderComponent( + component: AnyComponent(HorizontalTabsComponent( + context: nil, theme: component.theme, - items: component.items.map { item -> StorageUsageHeaderComponent.Item in - return StorageUsageHeaderComponent.Item( + tabs: component.items.map { item -> HorizontalTabsComponent.Tab in + return HorizontalTabsComponent.Tab( id: item.id, - title: item.title + content: .title(HorizontalTabsComponent.Tab.Title(text: item.title, entities: [], enableAnimations: false)), + badge: nil, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + if component.items.contains(where: { $0.id == item.id }) { + self.currentId = item.id + let transition = ComponentTransition(animation: .curve(duration: 0.35, curve: .spring)) + self.state?.updated(transition: transition) + component.currentPanelUpdated(item.id, transition) + } + } ) }, - activeIndex: currentIndex ?? 0, - transitionFraction: self.transitionFraction, - switchToPanel: { [weak self] id in - guard let self, let component = self.component else { - return - } - if component.items.contains(where: { $0.id == id }) { - self.currentId = id - let transition = ComponentTransition(animation: .curve(duration: 0.35, curve: .spring)) - self.state?.updated(transition: transition) - component.currentPanelUpdated(id, transition) - } - } + selectedTab: self.currentId, + isEditing: false, + layout: .fit, + liftWhileSwitching: true )), environment: {}, - containerSize: topPanelFrame.size + containerSize: tabsContainerSize ) - if let headerView = self.header.view { - if headerView.superview == nil { - self.addSubview(headerView) + + let tabContainerFrame = CGRect( + origin: CGPoint( + x: floorToScreenPixels((availableSize.width - tabsContainerEffectiveSize.width) / 2.0), + y: tabsTopInset + ), + size: tabsContainerEffectiveSize + ) + + transition.setFrame(view: self.tabsBackgroundContainer, frame: tabContainerFrame) + self.tabsBackgroundContainer.update(size: tabContainerFrame.size, isDark: component.theme.overallDarkAppearance, transition: transition) + + transition.setFrame(view: self.tabsBackgroundView, frame: CGRect(origin: CGPoint(), size: tabContainerFrame.size)) + self.tabsBackgroundView.update(size: tabContainerFrame.size, cornerRadius: tabContainerFrame.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel), transition: transition) + + if let tabsContainerView = self.tabsContainer.view as? HorizontalTabsComponent.View { + if tabsContainerView.superview == nil { + self.tabsBackgroundView.contentView.addSubview(tabsContainerView) + tabsContainerView.setOverlayContainerView(overlayContainerView: self) } - transition.setFrame(view: headerView, frame: topPanelFrame) + transition.setFrame(view: tabsContainerView, frame: CGRect(origin: CGPoint(), size: tabContainerFrame.size)) + tabsContainerView.updateTabSwitchFraction(fraction: self.transitionFraction, isDragging: self.isDraggingTabs, transition: transition) } + + let effectiveTabsHeight = tabsTopInset + tabContainerFrame.height + tabsBottomInset let childEnvironment = StorageUsagePanelEnvironment( theme: component.theme, strings: component.strings, dateTimeFormat: component.dateTimeFormat, - containerInsets: UIEdgeInsets(top: 0.0, left: component.insets.left, bottom: component.insets.bottom, right: component.insets.right), + containerInsets: UIEdgeInsets(top: effectiveTabsHeight, left: component.insets.left, bottom: component.insets.bottom, right: component.insets.right), isScrollable: environment.isScrollable ) - let centralPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelFrame.maxY)) + let centralPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)) if self.animatingTransition { visibleIds = visibleIds.filter({ self.visiblePanels[$0] != nil }) @@ -721,7 +485,7 @@ final class StorageUsagePanelContainerComponent: Component { ) if let panelView = panel.view { if panelView.superview == nil { - self.insertSubview(panelView, belowSubview: self.topPanelBackgroundView) + self.insertSubview(panelView, belowSubview: self.tabsBackgroundContainer) } panelTransition.setFrame(view: panelView, frame: itemFrame, completion: { [weak self] _ in diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift index 827f5feb2d..a7c10155d9 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift @@ -1122,8 +1122,8 @@ final class StorageUsageScreenComponent: Component { } var wasLockedAtPanels = false - if let panelContainerView = self.panelContainer.view, let navigationMetrics = self.navigationMetrics { - if self.scrollView.bounds.minY > 0.0 && abs(self.scrollView.bounds.minY - (panelContainerView.frame.minY - navigationMetrics.navigationHeight)) <= UIScreenPixel { + if let panelContainerView = self.panelContainer.view { + if self.scrollView.bounds.minY > 0.0 && abs(self.scrollView.bounds.minY - (panelContainerView.frame.minY - 0.0)) <= UIScreenPixel { wasLockedAtPanels = true } } @@ -2214,7 +2214,7 @@ final class StorageUsageScreenComponent: Component { theme: environment.theme, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat, - insets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: bottomInset, right: environment.safeInsets.right), + insets: UIEdgeInsets(top: environment.navigationHeight, left: environment.safeInsets.left, bottom: bottomInset, right: environment.safeInsets.right), items: panelItems, currentPanelUpdated: { [weak self] id, transition in guard let self else { @@ -2227,20 +2227,20 @@ final class StorageUsageScreenComponent: Component { environment: { StorageUsagePanelContainerEnvironment(isScrollable: wasLockedAtPanels) }, - containerSize: CGSize(width: availableSize.width, height: availableSize.height - environment.navigationHeight) + containerSize: CGSize(width: availableSize.width, height: availableSize.height) ) if let panelContainerView = self.panelContainer.view { if panelContainerView.superview == nil { self.scrollContainerView.addSubview(panelContainerView) } - transition.setFrame(view: panelContainerView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: panelContainerSize)) + transition.setFrame(view: panelContainerView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight - environment.navigationHeight), size: panelContainerSize)) if self.topContentOverlayView.superview == nil { self.scrollContainerView.insertSubview(self.topContentOverlayView, belowSubview: panelContainerView) } self.topContentOverlayView.backgroundColor = environment.theme.list.blocksBackgroundColor transition.setFrame(view: self.topContentOverlayView, frame: CGRect(origin: CGPoint(x: 0.0, y: panelContainerView.frame.minY - availableSize.height), size: availableSize)) } - contentHeight += panelContainerSize.height + contentHeight += panelContainerSize.height - environment.navigationHeight } else { self.panelContainer.view?.removeFromSuperview() self.topContentOverlayView.removeFromSuperview() @@ -2259,7 +2259,7 @@ final class StorageUsageScreenComponent: Component { var scrollViewBounds = self.scrollView.bounds scrollViewBounds.size = availableSize if wasLockedAtPanels, let panelContainerView = self.panelContainer.view { - scrollViewBounds.origin.y = panelContainerView.frame.minY - environment.navigationHeight + scrollViewBounds.origin.y = panelContainerView.frame.minY } transition.setBounds(view: self.scrollView, bounds: scrollViewBounds) diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift index d09670712e..fc854dfdb9 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift @@ -554,85 +554,96 @@ func presentChatReplyOptions(selfController: ChatControllerImpl, sourceView: UIV func moveReplyMessageToAnotherChat(selfController: ChatControllerImpl, replySubject: ChatInterfaceState.ReplyMessageSubject) { let _ = selfController.presentVoiceMessageDiscardAlert(action: { [weak selfController] in - guard let selfController else { - return - } - let filter: ChatListNodePeersFilter = [.onlyWriteable, .excludeDisabled, .doNotSearchMessages] - var attemptSelectionImpl: ((EnginePeer, ChatListDisabledPeerReason) -> Void)? - let controller = selfController.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams( - context: selfController.context, - updatedPresentationData: selfController.updatedPresentationData, - filter: filter, - hasFilters: true, - title: selfController.presentationData.strings.Conversation_MoveReplyToAnotherChatTitle, - attemptSelection: { peer, _, reason in - attemptSelectionImpl?(peer, reason) - }, - multipleSelection: false, - forwardedMessageIds: [], - selectForumThreads: true - )) - let context = selfController.context - attemptSelectionImpl = { [weak selfController, weak controller] peer, reason in - guard let selfController, let controller = controller else { + Task { @MainActor in + guard let selfController else { return } - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - switch reason { - case .generic: - controller.present(textAlertController(context: context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: presentationData.strings.Forward_ErrorDisabledForChat, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - case .premiumRequired: - controller.forEachController { c in - if let c = c as? UndoOverlayController { - c.dismiss() + let filter: ChatListNodePeersFilter = [.onlyWriteable, .excludeDisabled, .doNotSearchMessages] + var attemptSelectionImpl: ((EnginePeer, ChatListDisabledPeerReason) -> Void)? + + var suggestedPeers: [EnginePeer] = [] + if let message = await selfController.context.engine.data.get( + TelegramEngine.EngineData.Item.Messages.Message(id: replySubject.messageId) + ).get(), case let .user(author) = message.author { + suggestedPeers.append(.user(author)) + } + + let controller = selfController.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams( + context: selfController.context, + updatedPresentationData: selfController.updatedPresentationData, + filter: filter, + hasFilters: true, + title: selfController.presentationData.strings.Conversation_MoveReplyToAnotherChatTitle, + attemptSelection: { peer, _, reason in + attemptSelectionImpl?(peer, reason) + }, + multipleSelection: false, + forwardedMessageIds: [], + selectForumThreads: true, + suggestedPeers: suggestedPeers + )) + let context = selfController.context + attemptSelectionImpl = { [weak selfController, weak controller] peer, reason in + guard let selfController, let controller = controller else { + return + } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + switch reason { + case .generic: + controller.present(textAlertController(context: context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: presentationData.strings.Forward_ErrorDisabledForChat, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + case .premiumRequired: + controller.forEachController { c in + if let c = c as? UndoOverlayController { + c.dismiss() + } + return true } - return true - } - - var hasAction = false - let premiumConfiguration = PremiumConfiguration.with(appConfiguration: selfController.context.currentAppConfiguration.with { $0 }) - if !premiumConfiguration.isPremiumDisabled { - hasAction = true - } - - controller.present(UndoOverlayController(presentationData: presentationData, content: .premiumPaywall(title: nil, text: presentationData.strings.Chat_ToastMessagingRestrictedToPremium_Text(peer.compactDisplayTitle).string, customUndoText: hasAction ? presentationData.strings.Chat_ToastMessagingRestrictedToPremium_Action : nil, timeout: nil, linkAction: { _ in - }), elevatedLayout: false, animateInAsReplacement: true, action: { [weak selfController, weak controller] action in - guard let selfController, let controller else { + + var hasAction = false + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: selfController.context.currentAppConfiguration.with { $0 }) + if !premiumConfiguration.isPremiumDisabled { + hasAction = true + } + + controller.present(UndoOverlayController(presentationData: presentationData, content: .premiumPaywall(title: nil, text: presentationData.strings.Chat_ToastMessagingRestrictedToPremium_Text(peer.compactDisplayTitle).string, customUndoText: hasAction ? presentationData.strings.Chat_ToastMessagingRestrictedToPremium_Action : nil, timeout: nil, linkAction: { _ in + }), elevatedLayout: false, animateInAsReplacement: true, action: { [weak selfController, weak controller] action in + guard let selfController, let controller else { + return false + } + if case .undo = action { + let premiumController = PremiumIntroScreen(context: selfController.context, source: .settings) + controller.push(premiumController) + } return false - } - if case .undo = action { - let premiumController = PremiumIntroScreen(context: selfController.context, source: .settings) - controller.push(premiumController) - } - return false - }), in: .current) + }), in: .current) + } } + controller.peerSelected = { [weak selfController, weak controller] peer, threadId in + guard let selfController, let strongController = controller else { + return + } + let peerId = peer.id + //let accountPeerId = selfController.context.account.peerId + + var isPinnedMessages = false + if case .pinnedMessages = selfController.presentationInterfaceState.subject { + isPinnedMessages = true + } + + if case .peer(peerId) = selfController.chatLocation, selfController.parentController == nil, !isPinnedMessages { + selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(replySubject).withoutSelectionState() }).updatedSearch(nil) }) + selfController.updateItemNodesSearchTextHighlightStates() + selfController.searchResultsController = nil + strongController.dismiss() + } else { + moveReplyToChat(selfController: selfController, peerId: peerId, threadId: threadId, replySubject: replySubject, completion: { [weak strongController] in + strongController?.dismiss() + }) + } + } + selfController.chatDisplayNode.dismissInput() + selfController.effectiveNavigationController?.pushViewController(controller) } - controller.peerSelected = { [weak selfController, weak controller] peer, threadId in - guard let selfController, let strongController = controller else { - return - } - let peerId = peer.id - //let accountPeerId = selfController.context.account.peerId - - var isPinnedMessages = false - if case .pinnedMessages = selfController.presentationInterfaceState.subject { - isPinnedMessages = true - } - - if case .peer(peerId) = selfController.chatLocation, selfController.parentController == nil, !isPinnedMessages { - selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(replySubject).withoutSelectionState() }).updatedSearch(nil) }) - selfController.updateItemNodesSearchTextHighlightStates() - selfController.searchResultsController = nil - strongController.dismiss() - } else { - moveReplyToChat(selfController: selfController, peerId: peerId, threadId: threadId, replySubject: replySubject, completion: { [weak strongController] in - strongController?.dismiss() - }) - } - } - selfController.chatDisplayNode.dismissInput() - selfController.effectiveNavigationController?.pushViewController(controller) }) } diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift index 08c8d27e4d..ba2ffa3754 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift @@ -189,7 +189,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { } placeholder = placeholderValue - let chatListNode = ChatListNode(context: context, location: .chatList(groupId: .root), chatListFilter: chatListFilter, previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? [], chatListFilters: chatListFilters, displayAutoremoveTimeout: chatSelection.displayAutoremoveTimeout, displayPresence: chatSelection.displayPresence), isPeerEnabled: isPeerEnabled, theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false, autoSetReady: true, isMainTab: false) + let chatListNode = ChatListNode(context: context, location: .chatList(groupId: .root), chatListFilter: chatListFilter, previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? [], topPeers: [], chatListFilters: chatListFilters, displayAutoremoveTimeout: chatSelection.displayAutoremoveTimeout, displayPresence: chatSelection.displayPresence), isPeerEnabled: isPeerEnabled, theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false, autoSetReady: true, isMainTab: false) chatListNode.passthroughPeerSelection = true chatListNode.disabledPeerSelected = { peer, _, reason in attemptDisabledItemSelection?(peer, reason) From e8510c1a8a7f20a39f570dba1aba777fb1116acb Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 31 Mar 2026 23:05:28 +0800 Subject: [PATCH 09/50] Fix search filter display --- submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index b736750998..1195cf23f3 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -829,6 +829,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo transition.updateFrame(node: self.filterContainerNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + filtersInsets.left, y: layout.size.height - filtersInsets.bottom - 40.0), size: CGSize(width: layout.size.width - (layout.safeInsets.left + filtersInsets.left) * 2.0, height: 40.0))) self.updateFilterContainerNode(layout: layout, transition: transition) + self.filterContainerNode.isHidden = !self.displaySearchFilters + if isFirstTime { self.filterContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.appearanceTimestamp = CACurrentMediaTime() From e18153302914469829949b62428d92548b951fff Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 31 Mar 2026 23:41:45 +0800 Subject: [PATCH 10/50] Bump versions --- versions.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/versions.json b/versions.json index c10d79500f..be341e5350 100644 --- a/versions.json +++ b/versions.json @@ -1,6 +1,6 @@ { - "app": "12.6", - "xcode": "26.2", + "app": "12.6.1", + "xcode": "26.4", "bazel": "8.4.2:45e9388abf21d1107e146ea366ad080eb93cb6a5f3a4a3b048f78de0bc3faffa", "macos": "26" } From 43e41b20d9db037c5986b0395a86eadd8ab263dd Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 31 Mar 2026 23:51:10 +0800 Subject: [PATCH 11/50] Update webrtc --- third-party/webrtc/webrtc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/third-party/webrtc/webrtc b/third-party/webrtc/webrtc index dfd6b604d7..d5c77d3588 160000 --- a/third-party/webrtc/webrtc +++ b/third-party/webrtc/webrtc @@ -1 +1 @@ -Subproject commit dfd6b604d7194a3d41614afa2c8abd8825a657aa +Subproject commit d5c77d3588c9353dd48b80430d2ffb41dafef177 From ff95bd97c9a7ecf701d7f4176868ae12661a0133 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 31 Mar 2026 23:06:05 +0200 Subject: [PATCH 12/50] Various fixes --- .../Sources/Resources/PresentationResourcesDevices.swift | 2 +- .../Sources/ChatMessagePollBubbleContentNode.swift | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesDevices.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesDevices.swift index 448102ebe8..3680c72a2a 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesDevices.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesDevices.swift @@ -11,7 +11,7 @@ public struct PresentationResourcesDevices { public static let macbook = renderSettingsIcon(name: "Settings/Devices/Mac", backgroundColors: [colorBlue]) public static let iOS = renderAttachAppIcon(iconImage: UIImage(bundleImageName: "Settings/Devices/iOS")) - public static let android = renderSettingsIcon(name: "Settings/Devices/Watch", backgroundColors: [colorGreen]) + public static let android = renderSettingsIcon(name: "Settings/Devices/Android", backgroundColors: [colorGreen]) public static let safari = renderAttachAppIcon(iconImage: UIImage(bundleImageName: "Settings/Devices/Safari")) public static let chrome = renderSettingsIcon(name: "Settings/Devices/Chrome", backgroundColors: [colorGreen]) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift index e13612c2b5..9ae6cb91f7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift @@ -2781,7 +2781,8 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { if let data = item.context.currentAppConfiguration.with({ $0 }).data, let value = data["poll_answers_max"] as? Double { maxPollOptions = Int(value) } - let displayAddOption = poll.openAnswers && !isClosed && poll.pollId.namespace == Namespaces.Media.CloudPoll && orderedPollOptions.count < maxPollOptions + + let displayAddOption = poll.openAnswers && !isClosed && poll.pollId.namespace == Namespaces.Media.CloudPoll && !Namespaces.Message.allScheduled.contains(item.message.id.namespace) && orderedPollOptions.count < maxPollOptions if displayAddOption { let addOptionResult = makeAddOptionLayout(item.context, item.presentationData, item.presentationData.strings, incoming, item.controllerInteraction.focusedTextInputIsMedia, currentNewOptionText, currentNewOptionAttachment, constrainedSize.width - layoutConstants.bubble.borderInset * 2.0) boundingSize.width = max(boundingSize.width, addOptionResult.minimumWidth + layoutConstants.bubble.borderInset * 2.0) From e7722d7b29c3b88ac950098b2d7de46c00703e62 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 1 Apr 2026 00:10:39 +0200 Subject: [PATCH 13/50] Various fixes --- .../Sources/ChatMessagePollBubbleContentNode.swift | 2 +- .../TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift index 9ae6cb91f7..2cbcaee4cd 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift @@ -1346,7 +1346,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { node.buttonNode.isAccessibilityElement = shouldHaveRadioNode let previousResultBarWidth = minBarWidth + floor((width - leftInset - rightInset - minBarWidth) * (currentResult?.normalized ?? 0.0)) - let previousFrame = CGRect(origin: CGPoint(x: leftInset, y: contentHeight - 6.0 - 1.0), size: CGSize(width: previousResultBarWidth, height: 6.0)) + let previousFrame = CGRect(origin: CGPoint(x: leftInset, y: contentLayoutHeight - 6.0 - 1.0), size: CGSize(width: previousResultBarWidth, height: 6.0)) node.resultBarNode.layer.animateSpring(from: NSValue(cgPoint: previousFrame.center), to: NSValue(cgPoint: node.resultBarNode.frame.center), keyPath: "position", duration: 0.6, damping: 110.0) node.resultBarNode.layer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: previousFrame.size)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: node.resultBarNode.frame.size)), keyPath: "bounds", duration: 0.6, damping: 110.0) diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 97d8168aaf..f7f85c0501 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -1056,7 +1056,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu if !self.bounds.contains(point) { return nil } - if point.y < self.historyFrameNode.frame.minY { + if self.albumArtBackground.superview == nil && point.y < self.historyFrameNode.frame.minY { return self.dimNode.view } return result From 629f2b1e5d7c887b5297490faefee107b750cf2d Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 1 Apr 2026 00:33:44 +0200 Subject: [PATCH 14/50] Various fixes --- .../Sources/ChatMessagePollBubbleContentNode.swift | 2 +- .../Sources/StoryContentCaptionComponent.swift | 6 +++--- .../Sources/StoryItemSetContainerComponent.swift | 10 ++++++---- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift index 2cbcaee4cd..bf7cdcfd7c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift @@ -1346,7 +1346,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { node.buttonNode.isAccessibilityElement = shouldHaveRadioNode let previousResultBarWidth = minBarWidth + floor((width - leftInset - rightInset - minBarWidth) * (currentResult?.normalized ?? 0.0)) - let previousFrame = CGRect(origin: CGPoint(x: leftInset, y: contentLayoutHeight - 6.0 - 1.0), size: CGSize(width: previousResultBarWidth, height: 6.0)) + let previousFrame = CGRect(origin: CGPoint(x: leftInset, y: contentLayoutHeight - 6.0 - 1.0), size: CGSize(width: previousResultBarWidth, height: 4.0)) node.resultBarNode.layer.animateSpring(from: NSValue(cgPoint: previousFrame.center), to: NSValue(cgPoint: node.resultBarNode.frame.center), keyPath: "position", duration: 0.6, damping: 110.0) node.resultBarNode.layer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: previousFrame.size)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: node.resultBarNode.frame.size)), keyPath: "bounds", duration: 0.6, damping: 110.0) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift index 49d6030c89..25cefe535d 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift @@ -70,7 +70,7 @@ final class StoryContentCaptionComponent: Component { let textSelectionAction: (NSAttributedString, TextSelectionAction) -> Void let controller: () -> ViewController? let openStory: (EnginePeer, EngineStoryItem?) -> Void - let openMusic: (FileMediaReference, UIView) -> Void + let openMusic: (TelegramMediaFile, UIView) -> Void init( externalState: ExternalState, @@ -89,7 +89,7 @@ final class StoryContentCaptionComponent: Component { textSelectionAction: @escaping (NSAttributedString, TextSelectionAction) -> Void, controller: @escaping () -> ViewController?, openStory: @escaping (EnginePeer, EngineStoryItem?) -> Void, - openMusic: @escaping (FileMediaReference, UIView) -> Void + openMusic: @escaping (TelegramMediaFile, UIView) -> Void ) { self.externalState = externalState self.context = context @@ -756,7 +756,7 @@ final class StoryContentCaptionComponent: Component { return } if let sourceView = self.musicPanel?.view { - self.component?.openMusic(.standalone(media: music), sourceView) + self.component?.openMusic(music, sourceView) } }, animateScale: false diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index e1b6ffa7c0..fa1efdfe9e 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -7547,8 +7547,8 @@ public final class StoryItemSetContainerComponent: Component { }) } - private func performMusicAction(file: FileMediaReference, sourceView: UIView, gesture: ContextGesture?) { - guard let component = self.component, let controller = component.controller() else { + private func performMusicAction(file: TelegramMediaFile, sourceView: UIView, gesture: ContextGesture?) { + guard let component = self.component, let controller = component.controller(), let peer = PeerReference(component.slice.peer._asPeer()) else { return } @@ -7556,6 +7556,8 @@ public final class StoryItemSetContainerComponent: Component { let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) + let fileReference: FileMediaReference = .story(peer: peer, id: component.slice.item.storyItem.id, media: file) + let items = component.context.engine.peers.savedMusicIds() |> take(1) |> map { [weak self] savedIds -> ContextController.Items in @@ -7580,7 +7582,7 @@ public final class StoryItemSetContainerComponent: Component { return } - let _ = component.context.engine.peers.addSavedMusic(file: file).start() + let _ = component.context.engine.peers.addSavedMusic(file: fileReference).start() guard let controller = component.controller() as? StoryContainerScreen else { return @@ -7638,7 +7640,7 @@ public final class StoryItemSetContainerComponent: Component { return } - let _ = component.context.engine.messages.enqueueOutgoingMessage(to: component.context.account.peerId, replyTo: nil, content: .file(file)).start() + let _ = component.context.engine.messages.enqueueOutgoingMessage(to: component.context.account.peerId, replyTo: nil, content: .file(fileReference)).start() guard let controller = component.controller() as? StoryContainerScreen else { return From 43c263cb6f7bdda9b9ec4e839b8248a53bda6df9 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 1 Apr 2026 00:58:16 +0200 Subject: [PATCH 15/50] Various fixes --- .../Sources/PeerInfoCoverComponent.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift index 8b389de408..06d3b681fe 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift @@ -526,7 +526,10 @@ public final class PeerInfoCoverComponent: Component { let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: -1000.0 + availableSize.height), size: CGSize(width: availableSize.width, height: 1000.0)) transition.containedViewLayoutTransition.updateFrameAdditive(view: self.backgroundView, frame: backgroundFrame) - let patternWidth: CGFloat = min(380.0, availableSize.width - 32.0) + var patternWidth: CGFloat = 380.0 + if case .managedBot = component.subject { + patternWidth = min(380.0, availableSize.width - 32.0) + } let avatarPatternFrame = CGSize(width: patternWidth, height: floor(component.defaultHeight * 1.0)).centered(around: component.avatarCenter) transition.setFrame(layer: self.avatarBackgroundPatternContentsLayer, frame: avatarPatternFrame) From c81015c003edd713e008efe141c3fd7f3c6f7c43 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 2 Apr 2026 19:20:15 +0200 Subject: [PATCH 16/50] Various fixes --- .../PresentationResourcesSettings.swift | 186 +++++++++++------- .../ChatMessagePollBubbleContentNode.swift | 2 +- .../Icons/Gradient.imageset/gradient.pdf | Bin 10722 -> 9299 bytes .../ChatControllerOpenPollContextMenu.swift | 22 ++- .../ChatControllerOpenTodoContextMenu.swift | 8 +- .../OverlayAudioPlayerControllerNode.swift | 7 +- .../OverlayAudioPlayerControlsNode.swift | 5 + 7 files changed, 149 insertions(+), 81 deletions(-) diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift index ddf1e274ac..944b7a2ee4 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift @@ -11,9 +11,6 @@ public func renderSettingsIcon(name: String, scaleFactor: CGFloat = 1.0, backgro context.clear(bounds) if let backgroundColors { - context.addPath(UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 8.0).cgPath) - context.clip() - var locations: [CGFloat] = [0.0, 1.0] let colors: [CGColor] = backgroundColors.map(\.cgColor) @@ -22,21 +19,17 @@ public func renderSettingsIcon(name: String, scaleFactor: CGFloat = 1.0, backgro context.drawLinearGradient(gradient, start: CGPoint(x: size.width, y: size.height), end: CGPoint(x: 0.0, y: 0.0), options: CGGradientDrawingOptions()) - context.resetClip() - if let gradientImage, let cgImage = gradientImage.cgImage { - context.saveGState() context.setBlendMode(.plusLighter) context.draw(cgImage, in: CGRect(origin: .zero, size: size)) - context.restoreGState() } if let backdropImage, let cgImage = backdropImage.cgImage { - context.saveGState() context.setBlendMode(.overlay) context.draw(cgImage, in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) - context.restoreGState() } + + context.setBlendMode(.normal) if let image = UIImage(bundleImageName: name), let maskImage = image.cgImage { let imageSize = CGSize(width: image.size.width * scaleFactor, height: image.size.height * scaleFactor) @@ -48,6 +41,19 @@ public func renderSettingsIcon(name: String, scaleFactor: CGFloat = 1.0, backgro context.fill(imageRect) context.restoreGState() } + + let outerPath = UIBezierPath(rect: CGRect(origin: .zero, size: size)) + let innerPath = UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 8.0) + outerPath.append(innerPath) + + context.saveGState() + outerPath.usesEvenOddFillRule = true + context.addPath(outerPath.cgPath) + context.clip(using: .evenOdd) + + context.setBlendMode(.clear) + context.fill(CGRect(origin: .zero, size: size)) + context.restoreGState() } else { if let image = UIImage(bundleImageName: name), let cgImage = image.cgImage { let imageSize: CGSize @@ -66,16 +72,11 @@ public func renderAttachAppIcon(iconImage: UIImage?) -> UIImage? { return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) context.clear(bounds) - - context.addPath(UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 8.0).cgPath) - context.clip() - + if let iconImage, let cgImage = iconImage.cgImage { context.draw(cgImage, in: CGRect(origin: .zero, size: size)) } - context.resetClip() - if let gradientImage, let cgImage = gradientImage.cgImage { context.saveGState() context.setBlendMode(.plusLighter) @@ -89,6 +90,19 @@ public func renderAttachAppIcon(iconImage: UIImage?) -> UIImage? { context.draw(cgImage, in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) context.restoreGState() } + + let outerPath = UIBezierPath(rect: CGRect(origin: .zero, size: size)) + let innerPath = UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 8.0) + outerPath.append(innerPath) + + context.saveGState() + outerPath.usesEvenOddFillRule = true + context.addPath(outerPath.cgPath) + context.clip(using: .evenOdd) + + context.setBlendMode(.clear) + context.fill(CGRect(origin: .zero, size: size)) + context.restoreGState() }) } @@ -156,11 +170,7 @@ public struct PresentationResourcesSettings { public static let premium = generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) context.clear(bounds) - - let path = UIBezierPath(roundedRect: bounds, cornerRadius: 8.0) - context.addPath(path.cgPath) - context.clip() - + let colorsArray: [CGColor] = [ UIColor(rgb: 0x6b93ff).cgColor, UIColor(rgb: 0x6b93ff).cgColor, @@ -173,62 +183,82 @@ public struct PresentationResourcesSettings { context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: CGGradientDrawingOptions()) if let gradientImage, let cgImage = gradientImage.cgImage { - context.saveGState() context.setBlendMode(.plusLighter) context.draw(cgImage, in: CGRect(origin: .zero, size: size)) - context.restoreGState() } if let backdropImage, let cgImage = backdropImage.cgImage { - context.saveGState() context.setBlendMode(.overlay) context.draw(cgImage, in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) - context.restoreGState() } + context.setBlendMode(.normal) + if let image = generateTintedImage(image: UIImage(bundleImageName: "Item List/Icons/Premium"), color: UIColor(rgb: 0xffffff)), let cgImage = image.cgImage { context.draw(cgImage, in: CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - image.size.width) / 2.0), y: floorToScreenPixels((bounds.height - image.size.height) / 2.0)), size: image.size)) } + + let outerPath = UIBezierPath(rect: CGRect(origin: .zero, size: size)) + let innerPath = UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 8.0) + outerPath.append(innerPath) + + context.saveGState() + outerPath.usesEvenOddFillRule = true + context.addPath(outerPath.cgPath) + context.clip(using: .evenOdd) + + context.setBlendMode(.clear) + context.fill(CGRect(origin: .zero, size: size)) + context.restoreGState() }) - public static let ton = generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in - let bounds = CGRect(origin: CGPoint(), size: size) - context.clear(bounds) - - let path = UIBezierPath(roundedRect: bounds, cornerRadius: 8.0) - context.addPath(path.cgPath) - context.clip() - - context.setFillColor(UIColor(rgb: 0x32ade6).cgColor) - context.fill(bounds) - - if let gradientImage, let cgImage = gradientImage.cgImage { - context.saveGState() - context.setBlendMode(.plusLighter) - context.draw(cgImage, in: CGRect(origin: .zero, size: size)) - context.restoreGState() - } - - if let backdropImage, let cgImage = backdropImage.cgImage { - context.saveGState() - context.setBlendMode(.overlay) - context.draw(cgImage, in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) - context.restoreGState() - } - - if let image = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonAbout"), color: UIColor(rgb: 0xffffff)), let cgImage = image.cgImage { - context.draw(cgImage, in: CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - image.size.width) / 2.0), y: floorToScreenPixels((bounds.height - image.size.height) / 2.0)), size: image.size)) - } - }) + public static let ton = renderSettingsIcon(name: "Ads/TonAbout", backgroundColors: [UIColor(rgb: 0x32ade6)]) + +// generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in +// let bounds = CGRect(origin: CGPoint(), size: size) +// context.clear(bounds) +// +// context.setFillColor(UIColor(rgb: 0x32ade6).cgColor) +// context.fill(bounds) +// +// if let gradientImage, let cgImage = gradientImage.cgImage { +// context.saveGState() +// context.setBlendMode(.plusLighter) +// context.draw(cgImage, in: CGRect(origin: .zero, size: size)) +// context.restoreGState() +// } +// +// if let backdropImage, let cgImage = backdropImage.cgImage { +// context.saveGState() +// context.setBlendMode(.overlay) +// context.draw(cgImage, in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) +// context.restoreGState() +// } +// +// context.setBlendMode(.normal) +// +// if let image = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonAbout"), color: UIColor(rgb: 0xffffff)), let cgImage = image.cgImage { +// context.draw(cgImage, in: CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - image.size.width) / 2.0), y: floorToScreenPixels((bounds.height - image.size.height) / 2.0)), size: image.size)) +// } +// +// let outerPath = UIBezierPath(rect: CGRect(origin: .zero, size: size)) +// let innerPath = UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 8.0) +// outerPath.append(innerPath) +// +// context.saveGState() +// outerPath.usesEvenOddFillRule = true +// context.addPath(outerPath.cgPath) +// context.clip(using: .evenOdd) +// +// context.setBlendMode(.clear) +// context.fill(CGRect(origin: .zero, size: size)) +// context.restoreGState() +// }) public static let stars = generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) context.clear(bounds) - let path = UIBezierPath(roundedRect: bounds, cornerRadius: 7.0) - context.addPath(path.cgPath) - context.clip() - let colorsArray: [CGColor] = [ UIColor(rgb: 0xfec80f).cgColor, UIColor(rgb: 0xdd6f12).cgColor @@ -239,32 +269,39 @@ public struct PresentationResourcesSettings { context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: CGGradientDrawingOptions()) if let gradientImage, let cgImage = gradientImage.cgImage { - context.saveGState() context.setBlendMode(.plusLighter) context.draw(cgImage, in: CGRect(origin: .zero, size: size)) - context.restoreGState() } if let backdropImage, let cgImage = backdropImage.cgImage { - context.saveGState() context.setBlendMode(.overlay) context.draw(cgImage, in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) - context.restoreGState() } + context.setBlendMode(.normal) + if let image = generateTintedImage(image: UIImage(bundleImageName: "Item List/Icons/Stars"), color: UIColor(rgb: 0xffffff)), let cgImage = image.cgImage { context.draw(cgImage, in: CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - image.size.width) / 2.0), y: floorToScreenPixels((bounds.height - image.size.height) / 2.0)), size: image.size)) } + + let outerPath = UIBezierPath(rect: CGRect(origin: .zero, size: size)) + let innerPath = UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 8.0) + outerPath.append(innerPath) + + context.saveGState() + outerPath.usesEvenOddFillRule = true + context.addPath(outerPath.cgPath) + context.clip(using: .evenOdd) + + context.setBlendMode(.clear) + context.fill(CGRect(origin: .zero, size: size)) + context.restoreGState() }) public static let premiumGift = generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) context.clear(bounds) - let path = UIBezierPath(roundedRect: bounds, cornerRadius: 8.0) - context.addPath(path.cgPath) - context.clip() - let colorsArray: [CGColor] = [ UIColor(rgb: 0x3ba1f2).cgColor, UIColor(rgb: 0x3ba1f2).cgColor, @@ -277,22 +314,33 @@ public struct PresentationResourcesSettings { context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: CGGradientDrawingOptions()) if let gradientImage, let cgImage = gradientImage.cgImage { - context.saveGState() context.setBlendMode(.plusLighter) context.draw(cgImage, in: CGRect(origin: .zero, size: size)) - context.restoreGState() } if let backdropImage, let cgImage = backdropImage.cgImage { - context.saveGState() context.setBlendMode(.overlay) context.draw(cgImage, in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) - context.restoreGState() } + context.setBlendMode(.normal) + if let image = generateTintedImage(image: UIImage(bundleImageName: "Item List/Icons/Gift"), color: UIColor(rgb: 0xffffff)), let cgImage = image.cgImage { context.draw(cgImage, in: CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - image.size.width) / 2.0), y: floorToScreenPixels((bounds.height - image.size.height) / 2.0)), size: image.size)) } + + let outerPath = UIBezierPath(rect: CGRect(origin: .zero, size: size)) + let innerPath = UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 8.0) + outerPath.append(innerPath) + + context.saveGState() + outerPath.usesEvenOddFillRule = true + context.addPath(outerPath.cgPath) + context.clip(using: .evenOdd) + + context.setBlendMode(.clear) + context.fill(CGRect(origin: .zero, size: size)) + context.restoreGState() }) public static let bot = renderSettingsIcon(name: "Item List/Icons/Bot", backgroundColors: [colorBlue]) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift index bf7cdcfd7c..50a72cb79d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift @@ -2782,7 +2782,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { maxPollOptions = Int(value) } - let displayAddOption = poll.openAnswers && !isClosed && poll.pollId.namespace == Namespaces.Media.CloudPoll && !Namespaces.Message.allScheduled.contains(item.message.id.namespace) && orderedPollOptions.count < maxPollOptions + let displayAddOption = poll.openAnswers && !isClosed && poll.pollId.namespace == Namespaces.Media.CloudPoll && !Namespaces.Message.allScheduled.contains(item.message.id.namespace) && item.message.forwardInfo == nil && orderedPollOptions.count < maxPollOptions if displayAddOption { let addOptionResult = makeAddOptionLayout(item.context, item.presentationData, item.presentationData.strings, incoming, item.controllerInteraction.focusedTextInputIsMedia, currentNewOptionText, currentNewOptionAttachment, constrainedSize.width - layoutConstants.bubble.borderInset * 2.0) boundingSize.width = max(boundingSize.width, addOptionResult.minimumWidth + layoutConstants.bubble.borderInset * 2.0) diff --git a/submodules/TelegramUI/Images.xcassets/Item List/Icons/Gradient.imageset/gradient.pdf b/submodules/TelegramUI/Images.xcassets/Item List/Icons/Gradient.imageset/gradient.pdf index c8bc2dbfa6427321fc8a7900f94ba6908e7cbed5..c1125541bba8c9b39056b4241e1d72ab1fcda320 100644 GIT binary patch delta 419 zcmaD9eA#1zg4pC=f?UkGT*i|>3JPoIa2Xru8GwNToRPx?XTljdTp;z6-6XA;jSMX} zH%qdyQDo41B@JdH1M|(dln%1in_3u{DnJ~O$IAtAj;V!_1-h8IiG>MiyLBh$&%nzAA$<}n85 cM#e^(3K~W*_T*KXx?C3KhNfJqs;>TS03Uc+*Z=?k delta 1922 zcmeHIJ5Izv3@uhr1kq4z#iEe_i4|fr_IxdB5Tc==r9rd{H$XW=E`mf!#TmE(EtkMa zHg@)Ngq_NIvE%prnfbZ4Z5Ns+gpfd`2OX!ksp^||@F&J&~ zKp4zR#R0yMj|Q^^pOHh+mV`Nz!D4*^m!b)56ql1hvVg@!tT{GWm&s>mykz4DpjgHb z@c(*t>c)A6zM_jt`74SdV1&hbkRXhh&`5(&R}rJHgNl_>jDEi@rIfbwtGjxuikC{m zS#hTJn8gU)QW`0sijYdP8r9HsgsZ9&YHgf!QyR>%qCi@swH1izLr+v7?#z;X(HUA) zJ$asq!Lc?%et>jJ#zcl;624;ebH=K~eI#Cfx|ycqa@h`Hr*gs?N`E9&Y~u;j4AI diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPollContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPollContextMenu.swift index 7d9eca234f..fe2a3f3ddc 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPollContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPollContextMenu.swift @@ -15,6 +15,7 @@ import Pasteboard import TelegramStringFormatting import TelegramPresentationData import AvatarNode +import ChatPresentationInterfaceState private enum OptionsId: Hashable { case item @@ -82,7 +83,12 @@ extension ChatControllerImpl { } } - if canReplyInChat(self.presentationInterfaceState, accountPeerId: self.context.account.peerId) { + var canReply = canReplyInChat(self.presentationInterfaceState, accountPeerId: self.context.account.peerId) + if !canSendMessagesToChat(self.presentationInterfaceState) && (self.presentationInterfaceState.copyProtectionEnabled || message.isCopyProtected()) { + canReply = false + } + + if canReply { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Poll_ReplyToOption, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reply"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] c, _ in @@ -165,13 +171,15 @@ extension ChatControllerImpl { if let addedByPeer, let date = pollOption.date { var canRemove = false - if poll.isCreator { - canRemove = true - } else if addedByPeer.id == self.context.account.peerId { - let pollConfiguration = PollConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) - let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) - if currentTime < date + pollConfiguration.pollOptionDeletePeriod { + if !poll.isClosed { + if poll.isCreator { canRemove = true + } else if addedByPeer.id == self.context.account.peerId { + let pollConfiguration = PollConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + if currentTime < date + pollConfiguration.pollOptionDeletePeriod { + canRemove = true + } } } if canRemove { diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTodoContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTodoContextMenu.swift index c48cef418d..8d88365bd7 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTodoContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTodoContextMenu.swift @@ -15,6 +15,7 @@ import ChatControllerInteraction import Pasteboard import TelegramStringFormatting import TelegramPresentationData +import ChatPresentationInterfaceState private enum OptionsId: Hashable { case item @@ -125,7 +126,12 @@ extension ChatControllerImpl { } } - if canReplyInChat(self.presentationInterfaceState, accountPeerId: self.context.account.peerId) { + var canReply = canReplyInChat(self.presentationInterfaceState, accountPeerId: self.context.account.peerId) + if !canSendMessagesToChat(self.presentationInterfaceState) && (self.presentationInterfaceState.copyProtectionEnabled || message.isCopyProtected()) { + canReply = false + } + + if canReply { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Todo_ReplyToItem, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reply"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] c, _ in diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index f7f85c0501..2fe3644275 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -588,10 +588,11 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu guard let self else { return false } + if self.historyFrameTopOverlayNode.bounds.contains(self.view.convert(point, to: self.historyFrameTopOverlayNode.view)) { + return true + } if self.controlsNode.bounds.contains(self.view.convert(point, to: self.controlsNode.view)) { - if self.controlsNode.frame.maxY <= self.historyNode.frame.minY { - return true - } + return true } return false } diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControlsNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControlsNode.swift index 295df935ca..fb1bb8ed30 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControlsNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControlsNode.swift @@ -953,6 +953,11 @@ final class OverlayAudioPlayerControlsNode: ASDisplayNode { } profileAudioTransition.updateFrame(view: profileAudioView, frame: profileAudioFrame) } + } else if let profileAudio = self.profileAudio { + self.profileAudio = nil + if let profileAudioView = profileAudio.view { + profileAudioView.removeFromSuperview() + } } return finalPanelHeight From 2bad906e88edbf4a859fe5d1fa9d5beeb8322527 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 2 Apr 2026 20:37:35 +0200 Subject: [PATCH 17/50] Fix stickers memory leak --- .../VideoAnimationCache/Sources/VideoAnimationCache.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift b/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift index 5eaa7014cb..6c14e57e09 100644 --- a/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift +++ b/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift @@ -47,7 +47,7 @@ public func cacheVideoAnimation(path: String, hintVP9: Bool, width: Int, height: return frameDuration }, proposedWidth: frame.width, proposedHeight: frame.height, insertKeyframe: false) - if firstFrameOnly { + if firstFrameOnly || frameSource.frameCount == 1 { break } } else { From 1a728382c4341be83adaf9e6d9fc1dfbcb6206d5 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 3 Apr 2026 13:15:06 +0800 Subject: [PATCH 18/50] Disable AI button in secret chats --- submodules/AttachmentUI/Sources/AttachmentPanel.swift | 2 +- .../Sources/LegacyMessageInputPanel.swift | 2 +- submodules/TelegramUI/Sources/ChatControllerNode.swift | 2 +- .../TelegramUI/Sources/ChatInterfaceStateInputPanels.swift | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index b824d68588..2b56d11c74 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -2226,7 +2226,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog } }, makeEntityInputView: self.makeEntityInputView) if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["ios_disable_ai_chat"] as? Double, value == 1.0 { - } else { + } else if let peerId = self.presentationInterfaceState.chatLocation.peerId, peerId.namespace != Namespaces.Peer.SecretChat { textInputPanelNode.isAIEnabled = true } textInputPanelNode.interfaceInteraction = self.interfaceInteraction diff --git a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift index 2bf5c939d3..4801c4627f 100644 --- a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift +++ b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift @@ -83,7 +83,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { super.init() if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["ios_disable_ai_attach"] as? Double, value == 1.0 { - } else { + } else if let peerId = chatLocation.peerId, peerId.namespace != Namespaces.Peer.SecretChat { self.isAIEnabled = true } diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 5f2ab2b337..871545bc9b 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -888,7 +888,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self?.interfaceInteraction?.presentController(controller, nil) }) if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["ios_disable_ai_chat"] as? Double, value == 1.0 { - } else { + } else if let peerId = self.presentationInterfaceState.chatLocation.peerId, peerId.namespace != Namespaces.Peer.SecretChat { self.textInputPanelNode?.isAIEnabled = true } self.textInputPanelNode?.textInputAccessoryPanel = textInputAccessoryPanel diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift index 8a6217278f..9d76a5379a 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift @@ -465,7 +465,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState interfaceInteraction?.presentController(controller, nil) }) if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["ios_disable_ai_chat"] as? Double, value == 1.0 { - } else { + } else if let peerId = chatPresentationInterfaceState.chatLocation.peerId, peerId.namespace != Namespaces.Peer.SecretChat { panel.isAIEnabled = true } panel.textInputAccessoryPanel = textInputAccessoryPanel From 16c630b9469de8e3de459f6c0f10c4e448d50516 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 3 Apr 2026 15:50:39 +0800 Subject: [PATCH 19/50] Refactoring --- .../Sources/AccountContext.swift | 2 +- .../Sources/ShareController.swift | 145 ++++++++++++++ .../Sources/BrowserDocumentContent.swift | 6 +- .../Sources/BrowserInstantPageContent.swift | 5 +- .../BrowserUI/Sources/BrowserPdfContent.swift | 6 +- .../BrowserUI/Sources/BrowserScreen.swift | 17 +- .../BrowserUI/Sources/BrowserWebContent.swift | 7 +- .../Sources/ChatListSearchContainerNode.swift | 5 +- .../ChatItemGalleryFooterContentNode.swift | 98 +++++----- .../Items/ChatExternalFileGalleryItem.swift | 3 +- .../Sources/Items/ChatImageGalleryItem.swift | 3 +- .../GameUI/Sources/GameControllerNode.swift | 10 +- .../Sources/ImportStickerPackController.swift | 1 - .../Sources/InstantPageControllerNode.swift | 15 +- .../InstantPageGalleryFooterContentNode.swift | 3 +- .../InstantPageReferenceControllerNode.swift | 3 +- .../FolderInviteLinkListController.swift | 17 +- .../Sources/InviteLinkInviteController.swift | 24 ++- .../Sources/InviteLinkListController.swift | 33 ++-- .../Sources/InviteLinkViewController.swift | 18 +- .../Sources/InviteRequestsController.swift | 1 - .../JoinLinkPreviewPeerContentNode.swift | 2 +- .../LanguageLinkPreviewContentNode.swift | 2 +- .../Sources/LocationViewController.swift | 4 +- .../Sources/PeerAvatarImageGalleryItem.swift | 6 +- .../Sources/ChannelVisibilityController.swift | 5 +- .../Sources/DeviceContactInfoController.swift | 2 +- .../Sources/UserInfoController.swift | 1 - .../ProxyListSettingsController.swift | 2 +- .../ProxyServerSettingsController.swift | 1 - ...hareProxyServerActionSheetController.swift | 1 - .../LocalizationListControllerNode.swift | 7 +- .../ArchivedStickerPacksController.swift | 3 +- .../InstalledStickerPacksController.swift | 4 +- .../Sources/ThemePickerController.swift | 12 +- .../Themes/ThemePreviewController.swift | 3 +- .../Themes/ThemeSettingsController.swift | 12 +- .../Sources/UsernameSetupController.swift | 6 +- .../Sources/ShareController.swift | 67 ------- .../Sources/ChannelStatsController.swift | 18 +- .../Sources/StickerPackScreen.swift | 18 +- .../Components/MediaStreamComponent.swift | 22 +-- .../Sources/VideoChatScreen.swift | 46 ++--- .../Sources/VoiceChatController.swift | 1 - .../Sources/VoiceChatJoinScreen.swift | 2 +- .../Sources/ChatQrCodeScreen.swift | 2 +- .../Sources/GiftAuctionBidScreen.swift | 117 ++++++------ .../Sources/GiftAuctionViewScreen.swift | 119 ++++++------ .../Sources/GiftViewScreen.swift | 120 ++++++------ .../LegacyCamera/Sources/LegacyCamera.swift | 4 +- .../Sources/PeerInfoScreen.swift | 27 ++- .../Sources/PeerInfoScreenOpenURL.swift | 23 +-- .../PeerInfoScreenPerformButtonAction.swift | 14 +- .../Sources/StorySearchGridScreen.swift | 3 +- .../Sources/PeerInfoGiftsPaneNode.swift | 124 ++++++------ .../Sources/PeerInfoStoryPaneNode.swift | 143 ++++++-------- .../PostSuggestionsSettingsScreen.swift | 18 +- .../ReportPeerDetailsActionSheetItem.swift | 2 +- .../Sources/BusinessLinksSetupScreen.swift | 3 +- .../Sources/WallpaperGalleryController.swift | 37 ++-- .../Sources/ThemeGridController.swift | 3 +- .../Sources/StoryContainerScreen.swift | 1 - ...StoryItemSetContainerViewSendMessage.swift | 180 +++++++++--------- .../TextProcessingTextAreaComponent.swift | 2 +- .../Chat/ChatControllerLoadDisplayNode.swift | 5 +- .../Chat/ChatControllerMediaRecording.swift | 1 - ...ChatControllerNavigationButtonAction.swift | 1 - .../Sources/Chat/ChatControllerOpenPeer.swift | 1 - ...atControllerOpenViewOnceMediaMessage.swift | 1 - .../Sources/Chat/ChatControllerReport.swift | 1 - .../Chat/ChatControllerThemeManagement.swift | 1 - .../ChatBusinessLinkTitlePanelNode.swift | 3 +- .../TelegramUI/Sources/ChatController.swift | 3 +- .../Sources/ChatControllerNode.swift | 2 +- .../ChatControllerOpenAttachmentMenu.swift | 2 +- .../ChatControllerOpenMessageShareMenu.swift | 47 ++--- .../Sources/ComposeController.swift | 2 +- .../TelegramUI/Sources/OpenAddContact.swift | 2 +- .../TelegramUI/Sources/OpenChatMessage.swift | 4 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 6 +- .../OverlayAudioPlayerController.swift | 12 +- .../Sources/SharedAccountContext.swift | 16 +- .../WebUI/Sources/WebAppController.swift | 6 +- 83 files changed, 838 insertions(+), 889 deletions(-) diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 99ffead8cf..3d5b79b61a 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1519,7 +1519,7 @@ public protocol SharedAccountContext: AnyObject { func makeGiftDemoScreen(context: AccountContext) -> ViewController func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController func makeContentReportScreen(context: AccountContext, subject: ReportContentSubject, forceDark: Bool, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void, requestSelectMessages: ((String, Data, String?) -> Void)?) - func makeShareController(context: AccountContext, subject: ShareControllerSubject, forceExternal: Bool, shareStory: (() -> Void)?, enqueued: (([PeerId], [Int64]) -> Void)?, actionCompleted: (() -> Void)?) -> ViewController + func makeShareController(context: AccountContext, params: ShareControllerParams) -> ViewController func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController func makeIncomingMessagePrivacyScreen(context: AccountContext, value: GlobalPrivacySettings.NonContactChatsPrivacy, exceptions: SelectivePrivacySettings, update: @escaping (GlobalPrivacySettings.NonContactChatsPrivacy) -> Void) -> ViewController diff --git a/submodules/AccountContext/Sources/ShareController.swift b/submodules/AccountContext/Sources/ShareController.swift index 6e33940d3e..5ef521a5d5 100644 --- a/submodules/AccountContext/Sources/ShareController.swift +++ b/submodules/AccountContext/Sources/ShareController.swift @@ -6,6 +6,7 @@ import TelegramPresentationData import TelegramUIPreferences import AnimationCache import MultiAnimationRenderer +import Display public enum StorySharingSubject { case messages([Message]) @@ -37,6 +38,43 @@ public protocol ShareControllerEnvironment: AnyObject { func donateSendMessageIntent(account: ShareControllerAccountContext, peerIds: [EnginePeer.Id]) } +public final class ShareControllerAppAccountContext: ShareControllerAccountContext { + public let context: AccountContext + + public var accountId: AccountRecordId { + return self.context.account.id + } + public var accountPeerId: EnginePeer.Id { + return self.context.account.stateManager.accountPeerId + } + public var stateManager: AccountStateManager { + return self.context.account.stateManager + } + public var engineData: TelegramEngine.EngineData { + return self.context.engine.data + } + public var animationCache: AnimationCache { + return self.context.animationCache + } + public var animationRenderer: MultiAnimationRenderer { + return self.context.animationRenderer + } + public var contentSettings: ContentSettings { + return self.context.currentContentSettings.with { $0 } + } + public var appConfiguration: AppConfiguration { + return self.context.currentAppConfiguration.with { $0 } + } + + public init(context: AccountContext) { + self.context = context + } + + public func resolveInlineStickers(fileIds: [Int64]) -> Signal<[Int64: TelegramMediaFile], NoError> { + return self.context.engine.stickers.resolveInlineStickers(fileIds: fileIds) + } +} + public enum ShareControllerExternalStatus { case preparing(Bool) case progress(Float) @@ -78,3 +116,110 @@ public enum ShareControllerSubject { case mapMedia(TelegramMediaMap) case fromExternal(Int, ([PeerId], [PeerId: Int64], [PeerId: StarsAmount], String, ShareControllerAccountContext, Bool) -> Signal) } + +public struct ShareControllerAction { + public let title: String + public let action: () -> Void + + public init(title: String, action: @escaping () -> Void) { + self.title = title + self.action = action + } +} + +public enum ShareControllerPreferredAction { + case `default` + case saveToCameraRoll + case custom(action: ShareControllerAction) +} + +public struct ShareControllerSegmentedValue { + public let title: String + public let subject: ShareControllerSubject + public let actionTitle: String + public let formatSendTitle: (Int) -> String + + public init(title: String, subject: ShareControllerSubject, actionTitle: String, formatSendTitle: @escaping (Int) -> String) { + self.title = title + self.subject = subject + self.actionTitle = actionTitle + self.formatSendTitle = formatSendTitle + } +} + +public final class ShareControllerParams { + public let subject: ShareControllerSubject + public let presetText: String? + public let preferredAction: ShareControllerPreferredAction + public let showInChat: ((Message) -> Void)? + public let fromForeignApp: Bool + public let segmentedValues: [ShareControllerSegmentedValue]? + public let externalShare: Bool + public let immediateExternalShare: Bool + public let immediatePeerId: PeerId? + public let updatedPresentationData: (initial: PresentationData, signal: Signal)? + public let forceTheme: PresentationTheme? + public let forcedActionTitle: String? + public let shareAsLink: Bool + public let collectibleItemInfo: TelegramCollectibleItemInfo? + + public let actionCompleted: (() -> Void)? + public let dismissed: ((Bool) -> Void)? + public let completed: (([PeerId]) -> Void)? + public let enqueued: (([PeerId], [Int64]) -> Void)? + public let shareStory: (() -> Void)? + public let debugAction: (() -> Void)? + public let onMediaTimestampLinkCopied: ((Int32?) -> Void)? + public weak var parentNavigationController: NavigationController? + public let canSendInHighQuality: Bool + + public init( + subject: ShareControllerSubject, + presetText: String? = nil, + preferredAction: ShareControllerPreferredAction = .default, + showInChat: ((Message) -> Void)? = nil, + fromForeignApp: Bool = false, + segmentedValues: [ShareControllerSegmentedValue]? = nil, + externalShare: Bool = true, + immediateExternalShare: Bool = false, + immediatePeerId: PeerId? = nil, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + forceTheme: PresentationTheme? = nil, + forcedActionTitle: String? = nil, + shareAsLink: Bool = false, + collectibleItemInfo: TelegramCollectibleItemInfo? = nil, + actionCompleted: (() -> Void)? = nil, + dismissed: ((Bool) -> Void)? = nil, + completed: (([PeerId]) -> Void)? = nil, + enqueued: (([PeerId], [Int64]) -> Void)? = nil, + shareStory: (() -> Void)? = nil, + debugAction: (() -> Void)? = nil, + onMediaTimestampLinkCopied: ((Int32?) -> Void)? = nil, + parentNavigationController: NavigationController? = nil, + canSendInHighQuality: Bool = false + ) { + self.subject = subject + self.presetText = presetText + self.preferredAction = preferredAction + self.showInChat = showInChat + self.fromForeignApp = fromForeignApp + self.segmentedValues = segmentedValues + self.externalShare = externalShare + self.immediateExternalShare = immediateExternalShare + self.immediatePeerId = immediatePeerId + self.updatedPresentationData = updatedPresentationData + self.forceTheme = forceTheme + self.forcedActionTitle = forcedActionTitle + self.shareAsLink = shareAsLink + self.collectibleItemInfo = collectibleItemInfo + self.actionCompleted = actionCompleted + self.dismissed = dismissed + self.completed = completed + self.enqueued = enqueued + self.shareStory = shareStory + self.debugAction = debugAction + self.onMediaTimestampLinkCopied = onMediaTimestampLinkCopied + self.parentNavigationController = parentNavigationController + self.canSendInHighQuality = canSendInHighQuality + } +} diff --git a/submodules/BrowserUI/Sources/BrowserDocumentContent.swift b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift index fedb0f2c10..c0305c7761 100644 --- a/submodules/BrowserUI/Sources/BrowserDocumentContent.swift +++ b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift @@ -13,7 +13,6 @@ import AccountContext import AppBundle import PromptUI import SafariServices -import ShareController import UndoUI import UrlEscaping @@ -410,10 +409,9 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate private func share(url: String) { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let shareController = ShareController(context: self.context, subject: .url(url)) - shareController.actionCompleted = { [weak self] in + let shareController = self.context.sharedContext.makeShareController(context: self.context, params: ShareControllerParams(subject: .url(url), actionCompleted: { [weak self] in self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) - } + })) self.present(shareController, nil) } diff --git a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift index 38beedd814..52ff3cecfc 100644 --- a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift +++ b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift @@ -16,7 +16,6 @@ import TranslateUI import ContextUI import Pasteboard import SaveToCameraRoll -import ShareController import SafariServices import LocationUI import OpenInExternalAppUI @@ -1182,7 +1181,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg } }), ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuShare, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuShare), action: { [weak self] in if let self, let (webPage, _) = self.webPage, let image = media.media._asMedia() as? TelegramMediaImage { - self.present(ShareController(context: self.context, subject: .image(image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.media(media: .webPage(webPage: WebpageReference(webPage), media: image), resource: $0.resource)) }))), nil) + self.present(self.context.sharedContext.makeShareController(context: self.context, params: ShareControllerParams(subject: .image(image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.media(media: .webPage(webPage: WebpageReference(webPage), media: image), resource: $0.resource)) })))), nil) } })], catchTapsOutside: true) self.present(controller, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in @@ -1325,7 +1324,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg } }), ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuShare, accessibilityLabel: strings.Conversation_ContextMenuShare), action: { [weak self] in if let strongSelf = self, let (webPage, _) = strongSelf.webPage, case let .Loaded(content) = webPage.content { - strongSelf.present(ShareController(context: strongSelf.context, subject: .quote(text: text, url: content.url)), nil) + strongSelf.present(strongSelf.context.sharedContext.makeShareController(context: strongSelf.context, params: ShareControllerParams(subject: .quote(text: text, url: content.url))), nil) } })] diff --git a/submodules/BrowserUI/Sources/BrowserPdfContent.swift b/submodules/BrowserUI/Sources/BrowserPdfContent.swift index 4a77353538..922aa0ddf2 100644 --- a/submodules/BrowserUI/Sources/BrowserPdfContent.swift +++ b/submodules/BrowserUI/Sources/BrowserPdfContent.swift @@ -12,7 +12,6 @@ import AccountContext import AppBundle import PromptUI import SafariServices -import ShareController import UndoUI import UrlEscaping import PDFKit @@ -549,10 +548,9 @@ final class BrowserPdfContent: UIView, BrowserContent, UIScrollViewDelegate, PDF private func share(url: String) { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let shareController = ShareController(context: self.context, subject: .url(url)) - shareController.actionCompleted = { [weak self] in + let shareController = self.context.sharedContext.makeShareController(context: self.context, params: ShareControllerParams(subject: .url(url), actionCompleted: { [weak self] in self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) - } + })) self.present(shareController, nil) } diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index 022acc615f..4349ea6aaa 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -9,7 +9,6 @@ import ComponentFlow import ViewControllerComponent import AccountContext import ContextUI -import ShareController import UndoUI import BundleIconComponent import TelegramUIPreferences @@ -576,8 +575,9 @@ public class BrowserScreen: ViewController, MinimizableController { } else { subject = .url(url) } - let shareController = ShareController(context: self.context, subject: subject) - shareController.completed = { [weak self] peerIds in + let shareController = self.context.sharedContext.makeShareController(context: self.context, params: ShareControllerParams(subject: subject, actionCompleted: { [weak self] in + self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + }, completed: { [weak self] peerIds in guard let self else { return } @@ -590,10 +590,10 @@ public class BrowserScreen: ViewController, MinimizableController { guard let self else { return } - + let peers = peerList.compactMap { $0 } let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - + let text: String var savedMessages = false if peerIds.count == 1, let peerId = peerIds.first, peerId == self.context.account.peerId && !isDocument { @@ -615,7 +615,7 @@ public class BrowserScreen: ViewController, MinimizableController { text = "" } } - + self.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in if savedMessages, let self, action == .info { let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) @@ -633,10 +633,7 @@ public class BrowserScreen: ViewController, MinimizableController { return false }), in: .current) }) - } - shareController.actionCompleted = { [weak self] in - self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) - } + })) self.controller?.present(shareController, in: .window(.root)) case .minimize: self.minimize() diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index baf4a556a0..afe1d9ed04 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -13,7 +13,7 @@ import AccountContext import AppBundle import PromptUI import SafariServices -import ShareController + import UndoUI import LottieComponent import MultilineTextComponent @@ -1580,10 +1580,9 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU private func share(url: String) { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let shareController = ShareController(context: self.context, subject: .url(url)) - shareController.actionCompleted = { [weak self] in + let shareController = self.context.sharedContext.makeShareController(context: self.context, params: ShareControllerParams(subject: .url(url), actionCompleted: { [weak self] in self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) - } + })) self.present(shareController, nil) } diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 1195cf23f3..abbe656e9a 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -26,7 +26,6 @@ import AppBundle import GalleryData import InstantPageUI import ChatInterfaceState -import ShareController import UndoUI import TextFormat import Postbox @@ -877,9 +876,9 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } |> deliverOnMainQueue).startStandalone(next: { messages in if let strongSelf = self, !messages.isEmpty { - let shareController = ShareController(context: strongSelf.context, subject: .messages(messages.sorted(by: { lhs, rhs in + let shareController = strongSelf.context.sharedContext.makeShareController(context: strongSelf.context, params: ShareControllerParams(subject: .messages(messages.sorted(by: { lhs, rhs in return lhs.index < rhs.index - }).map({ $0._asMessage() })), externalShare: true, immediateExternalShare: true) + }).map({ $0._asMessage() })), externalShare: true, immediateExternalShare: true)) strongSelf.dismissInput() strongSelf.present?(shareController, nil) } diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index cc8dd4f502..e6dab339fc 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -12,7 +12,6 @@ import TextFormat import TelegramStringFormatting import AccountContext import RadialStatusNode -import ShareController import OpenInExternalAppUI import AppBundle import LocalizedPeerData @@ -489,8 +488,8 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll let theme = defaultDarkPresentationTheme let updatedPresentationData: (initial: PresentationData, signal: Signal) = (self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), self.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) - let shareController = ShareController(context: self.context, subject: .text(text.string), externalShare: true, immediateExternalShare: false, updatedPresentationData: updatedPresentationData) - + let shareController = self.context.sharedContext.makeShareController(context: self.context, params: ShareControllerParams(subject: .text(text.string), externalShare: true, immediateExternalShare: false, updatedPresentationData: updatedPresentationData)) + self.controllerInteraction?.presentController(shareController, nil) case .lookup: let controller = UIReferenceLibraryViewController(term: text.string) @@ -1888,41 +1887,14 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } } - let shareController = ShareController(context: strongSelf.context, subject: subject, preferredAction: preferredAction, externalShare: hasExternalShare, forceTheme: forceTheme) - shareController.dismissed = { [weak self] _ in - self?.interacting?(false) - } - shareController.onMediaTimestampLinkCopied = { [weak self] timestamp in - guard let self else { - return - } - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let text: String - if let timestamp { - let startTimeString: String - let hours = timestamp / (60 * 60) - let minutes = timestamp % (60 * 60) / 60 - let seconds = timestamp % 60 - if hours != 0 { - startTimeString = String(format: "%d:%02d:%02d", hours, minutes, seconds) - } else { - startTimeString = String(format: "%d:%02d", minutes, seconds) - } - text = presentationData.strings.Conversation_VideoTimeLinkCopied(startTimeString).string - } else { - text = presentationData.strings.Conversation_LinkCopied - } - - self.controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: text), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return true }), nil) - } - - shareController.actionCompleted = { [weak self] in + let shareController = strongSelf.context.sharedContext.makeShareController(context: strongSelf.context, params: ShareControllerParams(subject: subject, preferredAction: preferredAction, externalShare: hasExternalShare, forceTheme: forceTheme, actionCompleted: { [weak self] in if let strongSelf = self, let actionCompletionText = actionCompletionText { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } strongSelf.controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .mediaSaved(text: actionCompletionText), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return true }), nil) } - } - shareController.completed = { [weak self] peerIds in + }, dismissed: { [weak self] _ in + self?.interacting?(false) + }, completed: { [weak self] peerIds in if let strongSelf = self { let _ = (strongSelf.context.engine.data.get( EngineDataList( @@ -1933,7 +1905,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll if let strongSelf = self { let peers = peerList.compactMap { $0 } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - + let text: String var savedMessages = false if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId { @@ -1958,12 +1930,33 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll text = "" } } - + strongSelf.controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: true, animateInAsReplacement: true, action: { _ in return false }), nil) } }) } - } + }, onMediaTimestampLinkCopied: { [weak self] timestamp in + guard let self else { + return + } + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let text: String + if let timestamp { + let startTimeString: String + let hours = timestamp / (60 * 60) + let minutes = timestamp % (60 * 60) / 60 + let seconds = timestamp % 60 + if hours != 0 { + startTimeString = String(format: "%d:%02d:%02d", hours, minutes, seconds) + } else { + startTimeString = String(format: "%d:%02d", minutes, seconds) + } + text = presentationData.strings.Conversation_VideoTimeLinkCopied(startTimeString).string + } else { + text = presentationData.strings.Conversation_LinkCopied + } + self.controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: text), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return true }), nil) + })) strongSelf.controllerInteraction?.presentController(shareController, nil) } else { var singleText = presentationData.strings.Media_ShareItem(1) @@ -1984,17 +1977,14 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll let shareAction: ([Message]) -> Void = { messages in if let strongSelf = self { - let shareController = ShareController(context: strongSelf.context, subject: .messages(messages), preferredAction: preferredAction, forceTheme: forceTheme) - shareController.dismissed = { [weak self] _ in - self?.interacting?(false) - } - shareController.actionCompleted = { [weak self, weak shareController] in - if let strongSelf = self, let shareController = shareController, shareController.actionIsMediaSaving { + let shareController = strongSelf.context.sharedContext.makeShareController(context: strongSelf.context, params: ShareControllerParams(subject: .messages(messages), preferredAction: preferredAction, forceTheme: forceTheme, actionCompleted: { [weak self] in + if let strongSelf = self { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } strongSelf.controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .mediaSaved(text: presentationData.strings.Gallery_ImageSaved), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return true }), nil) } - } - shareController.completed = { [weak self] peerIds in + }, dismissed: { [weak self] _ in + self?.interacting?(false) + }, completed: { [weak self] peerIds in if let strongSelf = self { let _ = (strongSelf.context.engine.data.get( EngineDataList( @@ -2005,7 +1995,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll if let strongSelf = self { let peers = peerList.compactMap { $0 } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - + let text: String var savedMessages = false if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId { @@ -2030,12 +2020,12 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll text = "" } } - + strongSelf.controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: true, animateInAsReplacement: true, action: { _ in return false }), nil) } }) } - } + })) strongSelf.controllerInteraction?.presentController(shareController, nil) } } @@ -2142,11 +2132,9 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } } } - let shareController = ShareController(context: self.context, subject: subject, preferredAction: preferredAction, forceTheme: forceTheme) - shareController.dismissed = { [weak self] _ in + let shareController = self.context.sharedContext.makeShareController(context: self.context, params: ShareControllerParams(subject: subject, preferredAction: preferredAction, forceTheme: forceTheme, dismissed: { [weak self] _ in self?.interacting?(false) - } - shareController.completed = { [weak self] peerIds in + }, completed: { [weak self] peerIds in if let strongSelf = self { let _ = (strongSelf.context.engine.data.get( EngineDataList( @@ -2157,7 +2145,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll if let strongSelf = self { let peers = peerList.compactMap { $0 } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - + let text: String var savedMessages = false if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId { @@ -2182,12 +2170,12 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll text = "" } } - + strongSelf.controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: true, animateInAsReplacement: true, action: { _ in return false }), nil) } }) } - } + })) self.controllerInteraction?.presentController(shareController, nil) } } diff --git a/submodules/GalleryUI/Sources/Items/ChatExternalFileGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatExternalFileGalleryItem.swift index 54cb1eeeed..445d760853 100644 --- a/submodules/GalleryUI/Sources/Items/ChatExternalFileGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatExternalFileGalleryItem.swift @@ -9,7 +9,6 @@ import TelegramCore import TelegramPresentationData import AccountContext import RadialStatusNode -import ShareController class ChatExternalFileGalleryItem: GalleryItem { var id: AnyHashable { @@ -333,7 +332,7 @@ class ChatExternalFileGalleryItemNode: GalleryItemNode { @objc func actionButtonPressed() { if let (context, _) = self.contextAndFile, let message = self.message, let status = self.status, case .Local = status { let baseNavigationController = self.baseNavigationController() - (baseNavigationController?.topViewController as? ViewController)?.present(ShareController(context: context, subject: .messages([message]), showInChat: nil, externalShare: true, immediateExternalShare: true), in: .window(.root)) + (baseNavigationController?.topViewController as? ViewController)?.present(context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: .messages([message]), showInChat: nil, externalShare: true, immediateExternalShare: true)), in: .window(.root)) } } } diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index 9bd9bd3741..4f74fc0b96 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -17,7 +17,6 @@ import ImageContentAnalysis import TextSelectionNode import Speak import TranslateUI -import ShareController import UndoUI import ContextUI import SaveToCameraRoll @@ -388,7 +387,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { } case .share: if let controller = strongSelf.baseNavigationController()?.topViewController as? ViewController { - let shareController = ShareController(context: strongSelf.context, subject: .text(string), externalShare: true, immediateExternalShare: false, updatedPresentationData: (strongSelf.context.sharedContext.currentPresentationData.with({ $0 }), strongSelf.context.sharedContext.presentationData)) + let shareController = strongSelf.context.sharedContext.makeShareController(context: strongSelf.context, params: ShareControllerParams(subject: .text(string), externalShare: true, immediateExternalShare: false, updatedPresentationData: (strongSelf.context.sharedContext.currentPresentationData.with({ $0 }), strongSelf.context.sharedContext.presentationData))) controller.present(shareController, in: .window(.root)) } case .lookup: diff --git a/submodules/GameUI/Sources/GameControllerNode.swift b/submodules/GameUI/Sources/GameControllerNode.swift index 06923b10d1..6cadc8261d 100644 --- a/submodules/GameUI/Sources/GameControllerNode.swift +++ b/submodules/GameUI/Sources/GameControllerNode.swift @@ -9,6 +9,7 @@ import TelegramPresentationData import AccountContext import ShareController import UndoUI +import EmojiStatusComponent private class WeakGameScriptMessageHandler: NSObject, WKScriptMessageHandler { private let f: (WKScriptMessage) -> () @@ -144,7 +145,7 @@ final class GameControllerNode: ViewControllerTracingNode { if eventName == "share_game" || eventName == "share_score" { if let (botPeer, gameName) = self.shareData(), let addressName = botPeer.addressName, !addressName.isEmpty, !gameName.isEmpty { if eventName == "share_score" { - self.present(ShareController(context: self.context, subject: .fromExternal(1, { [weak self] peerIds, threadIds, requireStars, text, account, _ in + self.present(self.context.sharedContext.makeShareController(context: self.context, params: ShareControllerParams(subject: .fromExternal(1, { [weak self] peerIds, threadIds, requireStars, text, account, _ in if let strongSelf = self, let message = strongSelf.message, let account = account as? ShareControllerAppAccountContext { let signals = peerIds.map { TelegramEngine(account: account.context.account).messages.forwardGameWithScore(messageId: message.id, to: $0, threadId: threadIds[$0], as: nil) } return .single(.preparing(false)) @@ -158,7 +159,7 @@ final class GameControllerNode: ViewControllerTracingNode { } else { return .single(.done) } - }), showInChat: nil, externalShare: false, immediateExternalShare: false), nil) + }), showInChat: nil, externalShare: false, immediateExternalShare: false)), nil) } else { self.shareWithoutScore() } @@ -171,13 +172,12 @@ final class GameControllerNode: ViewControllerTracingNode { let url = "https://t.me/\(addressName)?game=\(gameName)" let context = self.context - let shareController = ShareController(context: context, subject: .url(url), showInChat: nil, externalShare: true) - shareController.actionCompleted = { [weak self] in + let shareController = context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: .url(url), showInChat: nil, externalShare: true, actionCompleted: { [weak self] in if let strongSelf = self { let presentationData = context.sharedContext.currentPresentationData.with { $0 } strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) } - } + })) self.present(shareController, nil) } } diff --git a/submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift b/submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift index a3792b02b7..3447530842 100644 --- a/submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift +++ b/submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift @@ -6,7 +6,6 @@ import TelegramCore import SwiftSignalKit import TelegramUIPreferences import AccountContext -import ShareController import StickerResources import AlertUI import PresentationDataUtils diff --git a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift index 528b6efceb..9e55514eeb 100644 --- a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift @@ -9,7 +9,6 @@ import SafariServices import TelegramPresentationData import TelegramUIPreferences import AccountContext -import ShareController import SaveToCameraRoll import GalleryUI import OpenInExternalAppUI @@ -142,14 +141,12 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { self.navigationBar.back = navigateBack self.navigationBar.share = { [weak self] in if let strongSelf = self, let (webPage, _) = strongSelf.webPage, case let .Loaded(content) = webPage.content { - let shareController = ShareController(context: context, subject: .url(content.url)) - shareController.actionCompleted = { [weak self] in + let shareController = context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: .url(content.url), actionCompleted: { [weak self] in if let strongSelf = self { let presentationData = context.sharedContext.currentPresentationData.with { $0 } strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) } - } - shareController.completed = { [weak self] peerIds in + }, completed: { [weak self] peerIds in let _ = (context.engine.data.get( EngineDataList( peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) @@ -159,7 +156,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { if let strongSelf = self { let peers = peerList.compactMap { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 } - + let text: String var savedMessages = false if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId { @@ -201,7 +198,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { }), nil) } }) - } + })) strongSelf.present(shareController, nil) } } @@ -1036,7 +1033,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { } }), ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in if let strongSelf = self, let (webPage, _) = strongSelf.webPage, case let .image(image) = media.media { - strongSelf.present(ShareController(context: strongSelf.context, subject: .image(image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.media(media: .webPage(webPage: WebpageReference(webPage), media: image), resource: $0.resource)) }))), nil) + strongSelf.present(strongSelf.context.sharedContext.makeShareController(context: strongSelf.context, params: ShareControllerParams(subject: .image(image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.media(media: .webPage(webPage: WebpageReference(webPage), media: image), resource: $0.resource)) })))), nil) } })], catchTapsOutside: true) self.present(controller, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in @@ -1150,7 +1147,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { } }), ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuShare, accessibilityLabel: strings.Conversation_ContextMenuShare), action: { [weak self] in if let strongSelf = self, let (webPage, _) = strongSelf.webPage, case let .Loaded(content) = webPage.content { - strongSelf.present(ShareController(context: strongSelf.context, subject: .quote(text: text, url: content.url)), nil) + strongSelf.present(strongSelf.context.sharedContext.makeShareController(context: strongSelf.context, params: ShareControllerParams(subject: .quote(text: text, url: content.url))), nil) } })] diff --git a/submodules/InstantPageUI/Sources/InstantPageGalleryFooterContentNode.swift b/submodules/InstantPageUI/Sources/InstantPageGalleryFooterContentNode.swift index 3f4da96123..4d6a0a3140 100644 --- a/submodules/InstantPageUI/Sources/InstantPageGalleryFooterContentNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageGalleryFooterContentNode.swift @@ -9,7 +9,6 @@ import Photos import TelegramPresentationData import TextFormat import AccountContext -import ShareController import GalleryUI import AppBundle @@ -147,7 +146,7 @@ final class InstantPageGalleryFooterContentNode: GalleryFooterContentNode { @objc func actionButtonPressed() { if let shareMedia = self.shareMedia { - self.controllerInteraction?.presentController(ShareController(context: self.context, subject: .media(shareMedia, nil), preferredAction: .saveToCameraRoll, showInChat: nil, externalShare: true, immediateExternalShare: false), nil) + self.controllerInteraction?.presentController(self.context.sharedContext.makeShareController(context: self.context, params: ShareControllerParams(subject: .media(shareMedia, nil), preferredAction: .saveToCameraRoll, showInChat: nil, externalShare: true, immediateExternalShare: false)), nil) } } } diff --git a/submodules/InstantPageUI/Sources/InstantPageReferenceControllerNode.swift b/submodules/InstantPageUI/Sources/InstantPageReferenceControllerNode.swift index 550e8c2ef5..50c1222805 100644 --- a/submodules/InstantPageUI/Sources/InstantPageReferenceControllerNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageReferenceControllerNode.swift @@ -6,7 +6,6 @@ import TelegramCore import SafariServices import TelegramPresentationData import AccountContext -import ShareController import OpenInExternalAppUI import TelegramUIPreferences @@ -414,7 +413,7 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, ASScrollVie UIPasteboard.general.string = text }), ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuShare, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuShare), action: { [weak self] in if let strongSelf = self, case let .Loaded(content) = strongSelf.webPage.webPage.content { - strongSelf.present(ShareController(context: strongSelf.context, subject: .quote(text: text, url: content.url)), nil) + strongSelf.present(strongSelf.context.sharedContext.makeShareController(context: strongSelf.context, params: ShareControllerParams(subject: .quote(text: text, url: content.url))), nil) } })]) controller.dismissed = { [weak self] in diff --git a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift index 4c7c859505..bde2b78d3a 100644 --- a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift @@ -17,7 +17,6 @@ import ContextUI import TelegramStringFormatting import ItemListPeerActionItem import ItemListPeerItem -import ShareController import UndoUI import QrCodeUI import PromptUI @@ -375,8 +374,10 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese } let arguments = FolderInviteLinkListControllerArguments(context: context, shareMainLink: { inviteLink in - let shareController = ShareController(context: context, subject: .url(inviteLink), updatedPresentationData: updatedPresentationData) - shareController.completed = { peerIds in + let shareController = context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: .url(inviteLink), updatedPresentationData: updatedPresentationData, actionCompleted: { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + }, completed: { peerIds in let _ = (context.engine.data.get( EngineDataList( peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) @@ -385,7 +386,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese |> deliverOnMainQueue).start(next: { peerList in let peers = peerList.compactMap { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 } - + let text: String var savedMessages = false if peerIds.count == 1, let peerId = peerIds.first, peerId == context.account.peerId { @@ -406,7 +407,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese text = "" } } - + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in if savedMessages, action == .info { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) @@ -423,11 +424,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese return false }), nil) }) - } - shareController.actionCompleted = { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) - } + })) presentControllerImpl?(shareController, nil) }, openMainLink: { _ in }, copyLink: { link in diff --git a/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift b/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift index f1776d198d..9533a925ec 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift @@ -14,7 +14,7 @@ import SectionHeaderItem import TelegramStringFormatting import MergeLists import ContextUI -import ShareController + import OverlayStatusController import PresentationDataUtils import DirectionalPanGesture @@ -540,8 +540,12 @@ public final class InviteLinkInviteController: ViewController { return } let updatedPresentationData = (strongSelf.presentationData, strongSelf.presentationDataPromise.get()) - let shareController = ShareController(context: context, subject: .url(inviteLink), updatedPresentationData: updatedPresentationData) - shareController.completed = { [weak self] peerIds in + let shareController = context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: .url(inviteLink), updatedPresentationData: updatedPresentationData, actionCompleted: { [weak self] in + if let strongSelf = self { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + } + }, completed: { [weak self] peerIds in if let strongSelf = self { let _ = (strongSelf.context.engine.data.get( EngineDataList( @@ -552,7 +556,7 @@ public final class InviteLinkInviteController: ViewController { if let strongSelf = self { let peers = peerList.compactMap { $0 } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - + let text: String var savedMessages = false if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId { @@ -573,7 +577,7 @@ public final class InviteLinkInviteController: ViewController { text = "" } } - + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in if savedMessages, let self, action == .info { let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) @@ -589,18 +593,12 @@ public final class InviteLinkInviteController: ViewController { } return false }), in: .window(.root)) - + strongSelf.controller?.dismiss() } }) } - } - shareController.actionCompleted = { [weak self] in - if let strongSelf = self { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) - } - } + })) strongSelf.controller?.present(shareController, in: .window(.root)) }, manageLinks: { [weak self] in guard let strongSelf = self else { diff --git a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift index bbaa98d914..08f3c04323 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift @@ -17,7 +17,6 @@ import ContextUI import TelegramStringFormatting import ItemListPeerActionItem import ItemListPeerItem -import ShareController import UndoUI import QrCodeUI @@ -433,8 +432,10 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio guard let inviteLink = invite.link else { return } - let shareController = ShareController(context: context, subject: .url(inviteLink), updatedPresentationData: updatedPresentationData) - shareController.completed = { peerIds in + let shareController = context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: .url(inviteLink), updatedPresentationData: updatedPresentationData, actionCompleted: { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + }, completed: { peerIds in let _ = (context.engine.data.get( EngineDataList( peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) @@ -443,7 +444,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio |> deliverOnMainQueue).start(next: { peerList in let peers = peerList.compactMap { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 } - + let text: String var savedMessages = false if peerIds.count == 1, let peerId = peerIds.first, peerId == context.account.peerId { @@ -464,7 +465,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio text = "" } } - + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in if savedMessages, action == .info { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) @@ -481,11 +482,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio return false }), nil) }) - } - shareController.actionCompleted = { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) - } + })) presentControllerImpl?(shareController, nil) }, openMainLink: { invite in let controller = InviteLinkViewController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, invite: invite, invitationsContext: nil, revokedInvitationsContext: revokedInvitesContext, importersContext: nil) @@ -649,8 +646,10 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio return } - let shareController = ShareController(context: context, subject: .url(inviteLink), updatedPresentationData: updatedPresentationData) - shareController.completed = { peerIds in + let shareController = context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: .url(inviteLink), updatedPresentationData: updatedPresentationData, actionCompleted: { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + }, completed: { peerIds in let _ = (context.engine.data.get( EngineDataList( peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) @@ -659,7 +658,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio |> deliverOnMainQueue).start(next: { peerList in let peers = peerList.compactMap { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 } - + let text: String var savedMessages = false if peerIds.count == 1, let peerId = peerIds.first, peerId == context.account.peerId { @@ -680,7 +679,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio text = "" } } - + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in if savedMessages, action == .info { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) @@ -697,11 +696,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio return false }), nil) }) - } - shareController.actionCompleted = { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) - } + })) presentControllerImpl?(shareController, nil) }))) diff --git a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift index 41a6025c11..e60ff909ad 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift @@ -14,7 +14,7 @@ import SectionHeaderItem import TelegramStringFormatting import MergeLists import ContextUI -import ShareController + import OverlayStatusController import PresentationDataUtils import DirectionalPanGesture @@ -610,8 +610,10 @@ public final class InviteLinkViewController: ViewController { guard let inviteLink = invite.link else { return } - let shareController = ShareController(context: context, subject: .url(inviteLink)) - shareController.completed = { [weak self] peerIds in + let shareController = context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: .url(inviteLink), actionCompleted: { [weak self] in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + }, completed: { [weak self] peerIds in if let strongSelf = self { let _ = (strongSelf.context.engine.data.get( EngineDataList( @@ -622,7 +624,7 @@ public final class InviteLinkViewController: ViewController { if let strongSelf = self { let peers = peerList.compactMap { $0 } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - + let text: String var savedMessages = false if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId { @@ -643,7 +645,7 @@ public final class InviteLinkViewController: ViewController { text = "" } } - + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in if savedMessages, let self, action == .info { let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) @@ -662,11 +664,7 @@ public final class InviteLinkViewController: ViewController { } }) } - } - shareController.actionCompleted = { [weak self] in - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) - } + })) self?.controller?.present(shareController, in: .window(.root)) }, editLink: { [weak self] invite in self?.editButtonPressed() diff --git a/submodules/InviteLinksUI/Sources/InviteRequestsController.swift b/submodules/InviteLinksUI/Sources/InviteRequestsController.swift index 8ab569cab3..a2ebd4868b 100644 --- a/submodules/InviteLinksUI/Sources/InviteRequestsController.swift +++ b/submodules/InviteLinksUI/Sources/InviteRequestsController.swift @@ -17,7 +17,6 @@ import ContextUI import TelegramStringFormatting import ItemListPeerActionItem import ItemListPeerItem -import ShareController import UndoUI private final class InviteRequestsControllerArguments { diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift index 7599eed5e5..a0ce070a00 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift @@ -7,11 +7,11 @@ import TelegramPresentationData import AvatarNode import AccountContext import SelectablePeerNode -import ShareController import SolidRoundedButtonNode import ActivityIndicator import ComponentFlow import EmojiStatusComponent +import ShareController private let avatarFont = avatarPlaceholderFont(size: 42.0) diff --git a/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewContentNode.swift b/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewContentNode.swift index e4c691aff3..cc143fa978 100644 --- a/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewContentNode.swift +++ b/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewContentNode.swift @@ -6,8 +6,8 @@ import TelegramCore import TelegramPresentationData import TextFormat import AccountContext -import ShareController import Markdown +import ShareController final class LanguageLinkPreviewContentNode: ASDisplayNode, ShareContentContainerNode { private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? diff --git a/submodules/LocationUI/Sources/LocationViewController.swift b/submodules/LocationUI/Sources/LocationViewController.swift index 519c96a91d..df3b15b229 100644 --- a/submodules/LocationUI/Sources/LocationViewController.swift +++ b/submodules/LocationUI/Sources/LocationViewController.swift @@ -11,7 +11,7 @@ import AppBundle import CoreLocation import PresentationDataUtils import OpenInExternalAppUI -import ShareController + import DeviceAccess import UndoUI import MapKit @@ -207,7 +207,7 @@ public final class LocationViewController: ViewController { } if let location = getLocation(from: strongSelf.subject) { let shareAction = OpenInControllerAction(title: strongSelf.presentationData.strings.Conversation_ContextMenuShare, action: { - strongSelf.present(ShareController(context: context, subject: .mapMedia(location), externalShare: true), in: .window(.root), with: nil) + strongSelf.present(context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: .mapMedia(location), externalShare: true)), in: .window(.root), with: nil) }) strongSelf.present(OpenInActionSheetController(context: context, updatedPresentationData: updatedPresentationData, item: .location(location: location, directions: nil), additionalAction: shareAction, openUrl: params.openUrl), in: .window(.root), with: nil) } diff --git a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift index 3d29daa132..911c1cd0d6 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift @@ -7,7 +7,6 @@ import TelegramCore import TelegramPresentationData import AccountContext import RadialStatusNode -import ShareController import PhotoResources import GalleryUI import TelegramUniversalVideoContent @@ -201,12 +200,11 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { forceTheme = defaultDarkColorPresentationTheme } - let shareController = ShareController(context: strongSelf.context, subject: subject, preferredAction: .saveToCameraRoll, forceTheme: forceTheme) - shareController.actionCompleted = { + let shareController = strongSelf.context.sharedContext.makeShareController(context: strongSelf.context, params: ShareControllerParams(subject: subject, preferredAction: .saveToCameraRoll, forceTheme: forceTheme, actionCompleted: { if let actionCompletionText = actionCompletionText { interaction.presentController(UndoOverlayController(presentationData: presentationData, content: .mediaSaved(text: actionCompletionText), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return true }), nil) } - } + })) interaction.presentController(shareController, nil) } } diff --git a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift index 5328856fd3..d79218d64a 100644 --- a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift @@ -1562,11 +1562,10 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta guard let inviteLink = invite.link else { return } - let shareController = ShareController(context: context, subject: .url(inviteLink), updatedPresentationData: updatedPresentationData) - shareController.actionCompleted = { + let shareController = context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: .url(inviteLink), updatedPresentationData: updatedPresentationData, actionCompleted: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) - } + })) presentControllerImpl?(shareController, nil) }, linkContextAction: { node, gesture in guard let node = node as? ContextReferenceContentNode, let controller = getControllerImpl?() else { diff --git a/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift b/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift index 73cdece2cb..6b83cecb5d 100644 --- a/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift +++ b/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift @@ -10,6 +10,7 @@ import ItemListUI import PresentationDataUtils import TelegramStringFormatting import AccountContext +import ShareController import AlertUI import PresentationDataUtils import PhotoResources @@ -24,7 +25,6 @@ import UndoUI import GalleryUI import PeerAvatarGalleryUI import Postbox -import ShareController import ContextUI private enum DeviceContactInfoAction { diff --git a/submodules/PeerInfoUI/Sources/UserInfoController.swift b/submodules/PeerInfoUI/Sources/UserInfoController.swift index 6f8b7694b2..be7a1488d0 100644 --- a/submodules/PeerInfoUI/Sources/UserInfoController.swift +++ b/submodules/PeerInfoUI/Sources/UserInfoController.swift @@ -13,7 +13,6 @@ import TextFormat import OverlayStatusController import TelegramStringFormatting import AccountContext -import ShareController import AlertUI import PresentationDataUtils import TelegramNotices diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift index 606fbfaf99..44e310c99e 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift @@ -9,8 +9,8 @@ import MtProtoKit import ItemListUI import PresentationDataUtils import AccountContext -import UrlEscaping import ShareController +import UrlEscaping private final class ProxySettingsControllerArguments { let toggleEnabled: (Bool) -> Void diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift index 5f35c78e37..b2ba003fb7 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift @@ -10,7 +10,6 @@ import PresentationDataUtils import AccountContext import UrlEscaping import UrlHandling -import ShareController private func shareLink(for server: ProxyServerSettings) -> String { var link: String diff --git a/submodules/SettingsUI/Sources/Data and Storage/ShareProxyServerActionSheetController.swift b/submodules/SettingsUI/Sources/Data and Storage/ShareProxyServerActionSheetController.swift index 56251e3eb4..1ac427d213 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ShareProxyServerActionSheetController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ShareProxyServerActionSheetController.swift @@ -7,7 +7,6 @@ import UIKit import SwiftSignalKit import TelegramPresentationData import QrCode -import ShareController public final class ShareProxyServerActionSheetController: ActionSheetController { private var presentationDisposable: Disposable? diff --git a/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift b/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift index d92950f226..2cb090e83a 100644 --- a/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift +++ b/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift @@ -10,7 +10,7 @@ import MergeLists import ItemListUI import PresentationDataUtils import AccountContext -import ShareController + import SearchBarNode import SearchUI import UndoUI @@ -793,13 +793,12 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { guard let strongSelf = self else { return } - let shareController = ShareController(context: strongSelf.context, subject: .url("https://t.me/setlanguage/\(info.languageCode)")) - shareController.actionCompleted = { [weak self] in + let shareController = strongSelf.context.sharedContext.makeShareController(context: strongSelf.context, params: ShareControllerParams(subject: .url("https://t.me/setlanguage/\(info.languageCode)"), actionCompleted: { [weak self] in if let strongSelf = self { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) } - } + })) strongSelf.present(shareController, nil) })) controller.setItemGroups([ diff --git a/submodules/SettingsUI/Sources/Stickers/ArchivedStickerPacksController.swift b/submodules/SettingsUI/Sources/Stickers/ArchivedStickerPacksController.swift index c028dbaacf..db9186d25d 100644 --- a/submodules/SettingsUI/Sources/Stickers/ArchivedStickerPacksController.swift +++ b/submodules/SettingsUI/Sources/Stickers/ArchivedStickerPacksController.swift @@ -13,7 +13,6 @@ import AccountContext import StickerPackPreviewUI import ItemListStickerPackItem import UndoUI -import ShareController public enum ArchivedStickerPacksControllerMode { case stickers @@ -565,7 +564,7 @@ public func archivedStickerPacksController(context: AccountContext, mode: Archiv } } let text = packNames.map { "https://t.me/addstickers/\($0)" }.joined(separator: "\n") - let shareController = ShareController(context: context, subject: .text(text), externalShare: true) + let shareController = context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: .text(text), externalShare: true)) presentControllerImpl?(shareController, nil) })]) } else { diff --git a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift index c8ae43fa25..b017259335 100644 --- a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift +++ b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift @@ -14,7 +14,7 @@ import StickerPackPreviewUI import ItemListStickerPackItem import ItemListPeerActionItem import UndoUI -import ShareController + import WebPBinding import ReactionImageComponent import FeaturedStickersScreen @@ -1063,7 +1063,7 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta } } let text = packNames.map { "https://t.me/addstickers/\($0)" }.joined(separator: "\n") - let shareController = ShareController(context: context, subject: .text(text), externalShare: true) + let shareController = context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: .text(text), externalShare: true)) presentControllerImpl?(shareController, nil) })]) } else { diff --git a/submodules/SettingsUI/Sources/ThemePickerController.swift b/submodules/SettingsUI/Sources/ThemePickerController.swift index 24c0d62467..b40046b0a5 100644 --- a/submodules/SettingsUI/Sources/ThemePickerController.swift +++ b/submodules/SettingsUI/Sources/ThemePickerController.swift @@ -13,7 +13,7 @@ import AlertUI import PresentationDataUtils import MediaResources import WallpaperResources -import ShareController + import AccountContext import ContextUI import UndoUI @@ -623,11 +623,10 @@ public func themePickerController(context: AccountContext, focusOnItemTag: Theme } items.append(.action(ContextMenuActionItem(text: presentationData.strings.Appearance_ShareTheme, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { c, f in c?.dismiss(completion: { - let shareController = ShareController(context: context, subject: .url("https://t.me/addtheme/\(theme.theme.slug)"), preferredAction: .default) - shareController.actionCompleted = { + let shareController = context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: .url("https://t.me/addtheme/\(theme.theme.slug)"), preferredAction: .default, actionCompleted: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) - } + })) presentControllerImpl?(shareController, nil) }) }))) @@ -876,11 +875,10 @@ public func themePickerController(context: AccountContext, focusOnItemTag: Theme } items.append(.action(ContextMenuActionItem(text: presentationData.strings.Appearance_ShareTheme, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { c, f in c?.dismiss(completion: { - let shareController = ShareController(context: context, subject: .url("https://t.me/addtheme/\(cloudTheme.theme.slug)"), preferredAction: .default) - shareController.actionCompleted = { + let shareController = context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: .url("https://t.me/addtheme/\(cloudTheme.theme.slug)"), preferredAction: .default, actionCompleted: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) - } + })) presentControllerImpl?(shareController, nil) }) }))) diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift index 26504db15a..dc564fdf2f 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift @@ -8,7 +8,6 @@ import TelegramCore import TelegramPresentationData import TelegramUIPreferences import AccountContext -import ShareController import CounterControllerTitleView import WallpaperResources import OverlayStatusController @@ -524,7 +523,7 @@ public final class ThemePreviewController: ViewController { subject = .media(media, nil) preferredAction = .default } - let controller = ShareController(context: self.context, subject: subject, preferredAction: preferredAction) + let controller = self.context.sharedContext.makeShareController(context: self.context, params: ShareControllerParams(subject: subject, preferredAction: preferredAction)) self.present(controller, in: .window(.root), blockInteraction: true) } } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift index 5e35e60c0d..ffe197cc66 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift @@ -13,7 +13,7 @@ import AlertUI import PresentationDataUtils import MediaResources import WallpaperResources -import ShareController + import AccountContext import ContextUI import UndoUI @@ -776,11 +776,10 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The } items.append(.action(ContextMenuActionItem(text: presentationData.strings.Appearance_ShareTheme, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { c, f in c?.dismiss(completion: { - let shareController = ShareController(context: context, subject: .url("https://t.me/addtheme/\(theme.theme.slug)"), preferredAction: .default) - shareController.actionCompleted = { + let shareController = context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: .url("https://t.me/addtheme/\(theme.theme.slug)"), preferredAction: .default, actionCompleted: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) - } + })) presentControllerImpl?(shareController, nil) }) }))) @@ -1027,11 +1026,10 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The } items.append(.action(ContextMenuActionItem(text: presentationData.strings.Appearance_ShareTheme, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { c, f in c?.dismiss(completion: { - let shareController = ShareController(context: context, subject: .url("https://t.me/addtheme/\(cloudTheme.theme.slug)"), preferredAction: .default) - shareController.actionCompleted = { + let shareController = context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: .url("https://t.me/addtheme/\(cloudTheme.theme.slug)"), preferredAction: .default, actionCompleted: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) - } + })) presentControllerImpl?(shareController, nil) }) }))) diff --git a/submodules/SettingsUI/Sources/UsernameSetupController.swift b/submodules/SettingsUI/Sources/UsernameSetupController.swift index 9af8ac6442..c3d3b1fef0 100644 --- a/submodules/SettingsUI/Sources/UsernameSetupController.swift +++ b/submodules/SettingsUI/Sources/UsernameSetupController.swift @@ -7,7 +7,6 @@ import TelegramPresentationData import ItemListUI import PresentationDataUtils import AccountContext -import ShareController import UndoUI import InviteLinksUI import TextFormat @@ -484,11 +483,10 @@ public func usernameSetupController(context: AccountContext, mode: UsernameSetup } if !currentAddressName.isEmpty { dismissInputImpl?() - let shareController = ShareController(context: context, subject: .url("https://t.me/\(currentAddressName)")) - shareController.actionCompleted = { + let shareController = context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: .url("https://t.me/\(currentAddressName)"), actionCompleted: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) - } + })) presentControllerImpl?(shareController, nil) } } diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index 3d0edf9a0c..76d9fff2c7 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -25,36 +25,6 @@ import ChatMessagePaymentAlertController private var ObjCKey_DeinitWatcher: Int? -public struct ShareControllerAction { - let title: String - let action: () -> Void - - public init(title: String, action: @escaping () -> Void) { - self.title = title - self.action = action - } -} - -public enum ShareControllerPreferredAction { - case `default` - case saveToCameraRoll - case custom(action: ShareControllerAction) -} - -public struct ShareControllerSegmentedValue { - let title: String - let subject: ShareControllerSubject - let actionTitle: String - let formatSendTitle: (Int) -> String - - public init(title: String, subject: ShareControllerSubject, actionTitle: String, formatSendTitle: @escaping (Int) -> String) { - self.title = title - self.subject = subject - self.actionTitle = actionTitle - self.formatSendTitle = formatSendTitle - } -} - private enum ExternalShareItem { case text(String) case url(URL) @@ -327,43 +297,6 @@ public final class ShareControllerAppEnvironment: ShareControllerEnvironment { } } -public final class ShareControllerAppAccountContext: ShareControllerAccountContext { - public let context: AccountContext - - public var accountId: AccountRecordId { - return self.context.account.id - } - public var accountPeerId: EnginePeer.Id { - return self.context.account.stateManager.accountPeerId - } - public var stateManager: AccountStateManager { - return self.context.account.stateManager - } - public var engineData: TelegramEngine.EngineData { - return self.context.engine.data - } - public var animationCache: AnimationCache { - return self.context.animationCache - } - public var animationRenderer: MultiAnimationRenderer { - return self.context.animationRenderer - } - public var contentSettings: ContentSettings { - return self.context.currentContentSettings.with { $0 } - } - public var appConfiguration: AppConfiguration { - return self.context.currentAppConfiguration.with { $0 } - } - - public init(context: AccountContext) { - self.context = context - } - - public func resolveInlineStickers(fileIds: [Int64]) -> Signal<[Int64: TelegramMediaFile], NoError> { - return self.context.engine.stickers.resolveInlineStickers(fileIds: fileIds) - } -} - public final class ShareControllerSwitchableAccount: Equatable { public let account: ShareControllerAccountContext public let peer: Peer diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index a0f5a6df6b..f470163bb9 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -18,7 +18,7 @@ import ContextUI import ItemListPeerItem import InviteLinksUI import UndoUI -import ShareController + import ItemListPeerActionItem import PremiumUI import StoryContainerScreen @@ -1940,8 +1940,10 @@ public func channelStatsController( let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.ChannelBoost_BoostLinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false })) }, shareBoostLink: { link in - let shareController = ShareController(context: context, subject: .url(link), updatedPresentationData: updatedPresentationData) - shareController.completed = { peerIds in + let shareController = context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: .url(link), updatedPresentationData: updatedPresentationData, actionCompleted: { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.ChannelBoost_BoostLinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false })) + }, completed: { peerIds in let _ = (context.engine.data.get( EngineDataList( peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) @@ -1950,7 +1952,7 @@ public func channelStatsController( |> deliverOnMainQueue).start(next: { peerList in let peers = peerList.compactMap { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 } - + let text: String var savedMessages = false if peerIds.count == 1, let peerId = peerIds.first, peerId == context.account.peerId { @@ -1971,7 +1973,7 @@ public func channelStatsController( text = "" } } - + presentImpl?(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in if savedMessages, action == .info { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) @@ -1985,11 +1987,7 @@ public func channelStatsController( return false })) }) - } - shareController.actionCompleted = { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.ChannelBoost_BoostLinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false })) - } + })) presentImpl?(shareController) }, openBoost: { boost in diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index 2621570cd5..ac6179b47e 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -1110,16 +1110,16 @@ private final class StickerPackContainer: ASDisplayNode { let parentNavigationController = strongSelf.controller?.parentNavigationController let shareController = strongSelf.context.sharedContext.makeShareController( context: strongSelf.context, - subject: shareSubject, - forceExternal: false, - shareStory: nil, - enqueued: nil, - actionCompleted: { [weak parentNavigationController] in - if let parentNavigationController = parentNavigationController, let controller = parentNavigationController.topViewController as? ViewController { - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - controller.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + params: ShareControllerParams( + subject: shareSubject, + externalShare: false, + actionCompleted: { [weak parentNavigationController] in + if let parentNavigationController = parentNavigationController, let controller = parentNavigationController.topViewController as? ViewController { + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + controller.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + } } - } + ) ) strongSelf.controller?.present(shareController, in: .window(.root)) } diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift index 24bb59e0de..d9fb545339 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift @@ -7,7 +7,7 @@ import SwiftSignalKit import AVKit import TelegramCore import Postbox -import ShareController + import UndoUI import TelegramPresentationData import PresentationDataUtils @@ -1202,8 +1202,12 @@ public final class MediaStreamComponentController: ViewControllerComponentContai var segmentedValues: [ShareControllerSegmentedValue]? segmentedValues = nil - let shareController = ShareController(context: strongSelf.context, subject: .url(inviteLinks.listenerLink), segmentedValues: segmentedValues, forceTheme: defaultDarkPresentationTheme, forcedActionTitle: presentationData.strings.VoiceChat_CopyInviteLink) - shareController.completed = { [weak self] peerIds in + let shareController = strongSelf.context.sharedContext.makeShareController(context: strongSelf.context, params: ShareControllerParams(subject: .url(inviteLinks.listenerLink), segmentedValues: segmentedValues, forceTheme: defaultDarkPresentationTheme, forcedActionTitle: presentationData.strings.VoiceChat_CopyInviteLink, actionCompleted: { [weak self] in + if let strongSelf = self { + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.VoiceChat_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + } + }, completed: { [weak self] peerIds in if let strongSelf = self { let _ = (strongSelf.context.engine.data.get( EngineDataList( @@ -1214,7 +1218,7 @@ public final class MediaStreamComponentController: ViewControllerComponentContai if let strongSelf = self { let peers = peerList.compactMap { $0 } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - + let text: String var isSavedMessages = false if peers.count == 1, let peer = peers.first { @@ -1231,18 +1235,12 @@ public final class MediaStreamComponentController: ViewControllerComponentContai } else { text = "" } - + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: isSavedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) } }) } - } - shareController.actionCompleted = { - if let strongSelf = self { - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.VoiceChat_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) - } - } + })) strongSelf.present(shareController, in: .window(.root)) } }) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index def60adbb4..bee8112161 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -18,7 +18,7 @@ import DeviceAccess import TelegramVoip import PresentationDataUtils import UndoUI -import ShareController + import AvatarNode import TelegramAudio import LegacyComponents @@ -825,8 +825,13 @@ final class VideoChatScreenComponent: Component { return formatSendTitle(environment.strings.VoiceChat_InviteLink_InviteListeners(Int32(count))) })] } - let shareController = ShareController(context: groupCall.accountContext, subject: .url(inviteLinks.listenerLink), segmentedValues: segmentedValues, forceTheme: environment.theme, forcedActionTitle: environment.strings.VoiceChat_CopyInviteLink) - shareController.completed = { [weak self] peerIds in + let shareController = groupCall.accountContext.sharedContext.makeShareController(context: groupCall.accountContext, params: ShareControllerParams(subject: .url(inviteLinks.listenerLink), segmentedValues: segmentedValues, forceTheme: environment.theme, forcedActionTitle: environment.strings.VoiceChat_CopyInviteLink, actionCompleted: { [weak self] in + guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else { + return + } + let presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) + self.presentToast(icon: .animation("anim_linkcopied"), text: presentationData.strings.VoiceChat_InviteLinkCopiedText, duration: 3) + }, completed: { [weak self] peerIds in guard let self, case let .group(groupCall) = self.currentCall else { return } @@ -839,10 +844,10 @@ final class VideoChatScreenComponent: Component { guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else { return } - + let peers = peerList.compactMap { $0 } let presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) - + let text: String var isSavedMessages = false if peers.count == 1, let peer = peers.first { @@ -861,14 +866,7 @@ final class VideoChatScreenComponent: Component { } self.presentToast(icon: .animation(isSavedMessages ? "anim_savedmessages" : "anim_forward"), text: text, duration: 3) }) - } - shareController.actionCompleted = { [weak self] in - guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else { - return - } - let presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) - self.presentToast(icon: .animation("anim_linkcopied"), text: presentationData.strings.VoiceChat_InviteLinkCopiedText, duration: 3) - } + })) environment.controller()?.present(shareController, in: .window(.root)) }) } else if groupCall.isConference { @@ -876,8 +874,13 @@ final class VideoChatScreenComponent: Component { return } - let shareController = ShareController(context: groupCall.accountContext, subject: .url(inviteLinks.listenerLink), forceTheme: environment.theme, forcedActionTitle: environment.strings.VoiceChat_CopyInviteLink) - shareController.completed = { [weak self] peerIds in + let shareController = groupCall.accountContext.sharedContext.makeShareController(context: groupCall.accountContext, params: ShareControllerParams(subject: .url(inviteLinks.listenerLink), forceTheme: environment.theme, forcedActionTitle: environment.strings.VoiceChat_CopyInviteLink, actionCompleted: { [weak self] in + guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else { + return + } + let presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) + self.presentToast(icon: .animation("anim_linkcopied"), text: presentationData.strings.VoiceChat_InviteLinkCopiedText, duration: 3) + }, completed: { [weak self] peerIds in guard let self, case let .group(groupCall) = self.currentCall else { return } @@ -890,10 +893,10 @@ final class VideoChatScreenComponent: Component { guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else { return } - + let peers = peerList.compactMap { $0 } let presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) - + let text: String var isSavedMessages = false if peers.count == 1, let peer = peers.first { @@ -912,14 +915,7 @@ final class VideoChatScreenComponent: Component { } self.presentToast(icon: .animation(isSavedMessages ? "anim_savedmessages" : "anim_forward"), text: text, duration: 3) }) - } - shareController.actionCompleted = { [weak self] in - guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else { - return - } - let presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) - self.presentToast(icon: .animation("anim_linkcopied"), text: presentationData.strings.VoiceChat_InviteLinkCopiedText, duration: 3) - } + })) environment.controller()?.present(shareController, in: .window(.root)) } } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 16b78d5194..1e883ee6f3 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -15,7 +15,6 @@ import MergeLists import ItemListUI import AppBundle import ContextUI -import ShareController import DeleteChatPeerActionSheetItem import UndoUI import AlertUI diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift b/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift index bfc203cdc3..fe3f0c60ce 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift @@ -10,9 +10,9 @@ import TelegramUIPreferences import AccountContext import AlertUI import PresentationDataUtils -import ShareController import AvatarNode import UndoUI +import ShareController public final class VoiceChatJoinScreen: ViewController { private var controllerNode: Node { diff --git a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift index f09dfe4ae6..3f0412ae82 100644 --- a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift @@ -24,7 +24,6 @@ import ShimmerEffect import WallpaperBackgroundNode import QrCode import AvatarNode -import ShareController import TelegramStringFormatting import PhotoResources import TextFormat @@ -40,6 +39,7 @@ import ComponentFlow import GlassBarButtonComponent import SheetComponent import BundleIconComponent +import ShareController private func closeButtonImage(theme: PresentationTheme) -> UIImage? { return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift index 5f2257cced..1cd138aaec 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift @@ -1726,69 +1726,70 @@ private final class GiftAuctionBidScreenComponent: Component { let shareController = context.sharedContext.makeShareController( context: context, - subject: .url(link), - forceExternal: false, - shareStory: nil, - enqueued: { [weak self, weak controller] peerIds, _ in - guard let self else { - return - } - let _ = (context.engine.data.get( - EngineDataList( - peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) - ) - ) - |> deliverOnMainQueue).startStandalone(next: { [weak self, weak controller] peerList in + params: ShareControllerParams( + subject: .url(link), + externalShare: false, + actionCompleted: { [weak controller] in + controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + }, + enqueued: { [weak self, weak controller] peerIds, _ in guard let self else { return } - let peers = peerList.compactMap { $0 } - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let text: String - var savedMessages = false - if peerIds.count == 1, let peerId = peerIds.first, peerId == context.account.peerId { - text = presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One - savedMessages = true - } else { - if peers.count == 1, let peer = peers.first { - var peerName = peer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - peerName = peerName.replacingOccurrences(of: "**", with: "") - text = presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string - } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { - var firstPeerName = firstPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "") - var secondPeerName = secondPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "") - text = presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string - } else if let peer = peers.first { - var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - peerName = peerName.replacingOccurrences(of: "**", with: "") - text = presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string + let _ = (context.engine.data.get( + EngineDataList( + peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) + ) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self, weak controller] peerList in + guard let self else { + return + } + let peers = peerList.compactMap { $0 } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let text: String + var savedMessages = false + if peerIds.count == 1, let peerId = peerIds.first, peerId == context.account.peerId { + text = presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One + savedMessages = true } else { - text = "" + if peers.count == 1, let peer = peers.first { + var peerName = peer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + var firstPeerName = firstPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "") + var secondPeerName = secondPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string + } else if let peer = peers.first { + var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string + } else { + text = "" + } } - } - - controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: false, action: { [weak self, weak controller] action in - if let self, savedMessages, action == .info { - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) - |> deliverOnMainQueue).start(next: { [weak self, weak controller] peer in - guard let peer else { - return - } - self?.openPeer(peer) - Queue.mainQueue().after(0.6) { - controller?.dismiss(animated: false, completion: nil) - } - }) - } - return false - }, additionalView: nil), in: .current) - }) - }, - actionCompleted: { [weak controller] in - controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - } + + controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: false, action: { [weak self, weak controller] action in + if let self, savedMessages, action == .info { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self, weak controller] peer in + guard let peer else { + return + } + self?.openPeer(peer) + Queue.mainQueue().after(0.6) { + controller?.dismiss(animated: false, completion: nil) + } + }) + } + return false + }, additionalView: nil), in: .current) + }) + } + ) ) controller.present(shareController, in: .window(.root)) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift index f0924cdd9b..07fe56a270 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift @@ -332,73 +332,74 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { let shareController = self.context.sharedContext.makeShareController( context: self.context, - subject: .url(link), - forceExternal: false, - shareStory: nil, - enqueued: { [weak self, weak controller] peerIds, _ in - guard let self else { - return - } - let _ = (self.context.engine.data.get( - EngineDataList( - peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) - ) - ) - |> deliverOnMainQueue).startStandalone(next: { [weak self, weak controller] peerList in + params: ShareControllerParams( + subject: .url(link), + externalShare: false, + actionCompleted: { [weak controller] in + controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + }, + enqueued: { [weak self, weak controller] peerIds, _ in guard let self else { return } - let peers = peerList.compactMap { $0 } - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let text: String - var savedMessages = false - if peerIds.count == 1, let peerId = peerIds.first, peerId == context.account.peerId { - text = presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One - savedMessages = true - } else { - if peers.count == 1, let peer = peers.first { - var peerName = peer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - peerName = peerName.replacingOccurrences(of: "**", with: "") - text = presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string - } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { - var firstPeerName = firstPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "") - var secondPeerName = secondPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "") - text = presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string - } else if let peer = peers.first { - var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - peerName = peerName.replacingOccurrences(of: "**", with: "") - text = presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string + let _ = (self.context.engine.data.get( + EngineDataList( + peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) + ) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self, weak controller] peerList in + guard let self else { + return + } + let peers = peerList.compactMap { $0 } + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let text: String + var savedMessages = false + if peerIds.count == 1, let peerId = peerIds.first, peerId == context.account.peerId { + text = presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One + savedMessages = true } else { - text = "" + if peers.count == 1, let peer = peers.first { + var peerName = peer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + var firstPeerName = firstPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "") + var secondPeerName = secondPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string + } else if let peer = peers.first { + var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string + } else { + text = "" + } } - } - - controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: false, action: { [weak self, weak controller] action in - if let self, savedMessages, action == .info { - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) - |> deliverOnMainQueue).start(next: { [weak self, weak controller] peer in - guard let peer else { - return - } - self?.openPeer(peer) - Queue.mainQueue().after(0.6) { - controller?.dismiss(animated: false, completion: nil) - } - }) - } - return false - }, additionalView: nil), in: .current) - }) - }, - actionCompleted: { [weak controller] in - controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - } + + controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: false, action: { [weak self, weak controller] action in + if let self, savedMessages, action == .info { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self, weak controller] peer in + guard let peer else { + return + } + self?.openPeer(peer) + Queue.mainQueue().after(0.6) { + controller?.dismiss(animated: false, completion: nil) + } + }) + } + return false + }, additionalView: nil), in: .current) + }) + } + ) ) controller.present(shareController, in: .window(.root)) } - + func openGiftResale() { guard let controller = self.getController() as? GiftAuctionViewScreen, let gift = self.giftAuctionState?.gift, case let .generic(gift) = gift else { return diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 0939675b93..c2eb599ab8 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -934,73 +934,75 @@ private final class GiftViewSheetContent: CombinedComponent { let link = "https://t.me/nft/\(gift.slug)" let shareController = self.context.sharedContext.makeShareController( context: self.context, - subject: .url(link), - forceExternal: false, - shareStory: shareStoryImpl, - enqueued: { [weak self, weak controller] peerIds, _ in - guard let self else { - return - } - let _ = (self.context.engine.data.get( - EngineDataList( - peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) - ) - ) - |> deliverOnMainQueue).startStandalone(next: { [weak self, weak controller] peerList in + params: ShareControllerParams( + subject: .url(link), + externalShare: false, + actionCompleted: { [weak controller] in + controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + }, + enqueued: { [weak self, weak controller] peerIds, _ in guard let self else { return } - let peers = peerList.compactMap { $0 } - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let text: String - var savedMessages = false - if peerIds.count == 1, let peerId = peerIds.first, peerId == context.account.peerId { - text = presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One - savedMessages = true - } else { - if peers.count == 1, let peer = peers.first { - var peerName = peer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - peerName = peerName.replacingOccurrences(of: "**", with: "") - text = presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string - } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { - var firstPeerName = firstPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "") - var secondPeerName = secondPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "") - text = presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string - } else if let peer = peers.first { - var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - peerName = peerName.replacingOccurrences(of: "**", with: "") - text = presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string + let _ = (self.context.engine.data.get( + EngineDataList( + peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) + ) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self, weak controller] peerList in + guard let self else { + return + } + let peers = peerList.compactMap { $0 } + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let text: String + var savedMessages = false + if peerIds.count == 1, let peerId = peerIds.first, peerId == context.account.peerId { + text = presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One + savedMessages = true } else { - text = "" + if peers.count == 1, let peer = peers.first { + var peerName = peer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + var firstPeerName = firstPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "") + var secondPeerName = secondPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string + } else if let peer = peers.first { + var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string + } else { + text = "" + } } - } - - controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: false, action: { [weak self, weak controller] action in - if let self, savedMessages, action == .info { - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) - |> deliverOnMainQueue).start(next: { [weak self, weak controller] peer in - guard let peer else { - return - } - self?.openPeer(peer) - Queue.mainQueue().after(0.6) { - controller?.dismiss(animated: false, completion: nil) - } - }) - } - return false - }, additionalView: nil), in: .current) - }) - }, - actionCompleted: { [weak controller] in - controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - } + + controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: false, action: { [weak self, weak controller] action in + if let self, savedMessages, action == .info { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self, weak controller] peer in + guard let peer else { + return + } + self?.openPeer(peer) + Queue.mainQueue().after(0.6) { + controller?.dismiss(animated: false, completion: nil) + } + }) + } + return false + }, additionalView: nil), in: .current) + }) + }, + shareStory: shareStoryImpl + ) ) controller.present(shareController, in: .window(.root)) } - + func setAsGiftTheme() { guard let arguments = self.subject.arguments, let controller = self.getController() as? GiftViewScreen, let navigationController = controller.navigationController as? NavigationController, case let .unique(gift) = arguments.gift else { return diff --git a/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift b/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift index 930f3250e0..1b55e61013 100644 --- a/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift +++ b/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift @@ -255,7 +255,7 @@ public func presentedLegacyShortcutCamera(context: AccountContext, saveCapturedM nativeGenerator(_1, _2, _3, nil) }) if let parentController = parentController { - parentController.present(ShareController(context: context, subject: .fromExternal(1, { peerIds, _, _, text, account, silently in + parentController.present(context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: .fromExternal(1, { peerIds, _, _, text, account, silently in guard let account = account as? ShareControllerAppAccountContext else { return .single(.done) } @@ -277,7 +277,7 @@ public func presentedLegacyShortcutCamera(context: AccountContext, saveCapturedM } |> then(.single(ShareControllerExternalStatus.done)) } - }), showInChat: nil, externalShare: false), in: .window(.root)) + }), showInChat: nil, externalShare: false)), in: .window(.root)) } } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index ab03540a15..e7c811e0b7 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -16,7 +16,6 @@ import PresentationDataUtils import NotificationMuteSettingsUI import NotificationSoundSelectionUI import OverlayStatusController -import ShareController import PhotoResources import PeerAvatarGalleryUI import TelegramIntents @@ -3603,8 +3602,12 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } func openShareLink(url: String) { - let shareController = ShareController(context: self.context, subject: .url(url), updatedPresentationData: self.controller?.updatedPresentationData) - shareController.completed = { [weak self] peerIds in + let shareController = self.context.sharedContext.makeShareController(context: self.context, params: ShareControllerParams(subject: .url(url), updatedPresentationData: self.controller?.updatedPresentationData, actionCompleted: { [weak self] in + if let strongSelf = self { + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + } + }, completed: { [weak self] peerIds in guard let strongSelf = self else { return } @@ -3617,10 +3620,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let strongSelf = self else { return } - + let peers = peerList.compactMap { $0 } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - + let text: String var savedMessages = false if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId { @@ -3641,7 +3644,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro text = "" } } - + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in if savedMessages, let self, action == .info { let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) @@ -3658,13 +3661,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return false }), in: .current) }) - } - shareController.actionCompleted = { [weak self] in - if let strongSelf = self { - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - } - } + })) self.view.endEditing(true) self.controller?.present(shareController, in: .window(.root)) } @@ -5552,9 +5549,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if let strongSelf = self, !messages.isEmpty { strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone, nil, nil) - let shareController = ShareController(context: strongSelf.context, subject: .messages(messages.sorted(by: { lhs, rhs in + let shareController = strongSelf.context.sharedContext.makeShareController(context: strongSelf.context, params: ShareControllerParams(subject: .messages(messages.sorted(by: { lhs, rhs in return lhs.index < rhs.index - }).map({ $0._asMessage() })), externalShare: true, immediateExternalShare: true, updatedPresentationData: strongSelf.controller?.updatedPresentationData) + }).map({ $0._asMessage() })), externalShare: true, immediateExternalShare: true, updatedPresentationData: strongSelf.controller?.updatedPresentationData)) strongSelf.view.endEditing(true) strongSelf.controller?.present(shareController, in: .window(.root)) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenURL.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenURL.swift index 476ce5a7f7..f9b34f3961 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenURL.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenURL.swift @@ -9,7 +9,6 @@ import OpenInExternalAppUI import PresentationDataUtils import OverlayStatusController import HashtagSearchUI -import ShareController import UndoUI extension PeerInfoScreenNode { @@ -222,8 +221,12 @@ extension PeerInfoScreenNode { guard let self else { return } - let shareController = ShareController(context: self.context, subject: .url(url), updatedPresentationData: self.controller?.updatedPresentationData, collectibleItemInfo: collectibleItemInfo) - shareController.completed = { [weak self] peerIds in + let shareController = self.context.sharedContext.makeShareController(context: self.context, params: ShareControllerParams(subject: .url(url), updatedPresentationData: self.controller?.updatedPresentationData, collectibleItemInfo: collectibleItemInfo, actionCompleted: { [weak self] in + if let strongSelf = self { + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + } + }, completed: { [weak self] peerIds in guard let strongSelf = self else { return } @@ -236,10 +239,10 @@ extension PeerInfoScreenNode { guard let strongSelf = self else { return } - + let peers = peerList.compactMap { $0 } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - + let text: String var savedMessages = false if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId { @@ -260,7 +263,7 @@ extension PeerInfoScreenNode { text = "" } } - + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in if savedMessages, let self, action == .info { let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) @@ -277,13 +280,7 @@ extension PeerInfoScreenNode { return false }), in: .current) }) - } - shareController.actionCompleted = { [weak self] in - if let strongSelf = self { - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - } - } + })) self.view.endEditing(true) self.controller?.present(shareController, in: .window(.root)) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenPerformButtonAction.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenPerformButtonAction.swift index 7d77f7bac8..d5f1c3f837 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenPerformButtonAction.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenPerformButtonAction.swift @@ -10,7 +10,6 @@ import ContextUI import TelegramPresentationData import NotificationPeerExceptionController import NotificationExceptionsScreen -import ShareController import TranslateUI import TelegramNotices import AlertComponent @@ -584,8 +583,7 @@ extension PeerInfoScreenNode { if let strongSelf = self { let contact = TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id, vCardData: nil) - let shareController = ShareController(context: strongSelf.context, subject: .media(.standalone(media: contact), nil), updatedPresentationData: strongSelf.controller?.updatedPresentationData) - shareController.completed = { [weak self] peerIds in + let shareController = strongSelf.context.sharedContext.makeShareController(context: strongSelf.context, params: ShareControllerParams(subject: .media(.standalone(media: contact), nil), updatedPresentationData: strongSelf.controller?.updatedPresentationData, completed: { [weak self] peerIds in if let strongSelf = self { let _ = (strongSelf.context.engine.data.get( EngineDataList( @@ -596,11 +594,11 @@ extension PeerInfoScreenNode { guard let strongSelf = self else { return } - + let peers = peerList.compactMap { $0 } - + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - + let text: String var savedMessages = false if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId { @@ -621,7 +619,7 @@ extension PeerInfoScreenNode { text = "" } } - + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in if savedMessages, let self, action == .info { let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) @@ -639,7 +637,7 @@ extension PeerInfoScreenNode { }), in: .current) }) } - } + })) strongSelf.controller?.present(shareController, in: .window(.root)) } }))) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift index 3a15d11259..b3d43bb29d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift @@ -16,7 +16,6 @@ import BottomButtonPanelComponent import UndoUI import MoreHeaderButton import SaveToCameraRoll -import ShareController import OpenInExternalAppUI final class StorySearchGridScreenComponent: Component { @@ -250,7 +249,7 @@ public final class StorySearchGridScreen: ViewControllerComponentContainer { guard let self else { return } - self.present(ShareController(context: self.context, subject: .mapMedia(locationMap), externalShare: true), in: .window(.root), with: nil) + self.present(self.context.sharedContext.makeShareController(context: self.context, params: ShareControllerParams(subject: .mapMedia(locationMap), externalShare: true)), in: .window(.root), with: nil) }) self.present(OpenInActionSheetController(context: self.context, updatedPresentationData: nil, item: .location(location: locationMap, directions: nil), additionalAction: shareAction, openUrl: { [weak self] url in guard let self else { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index e12b0171b2..bab566ae72 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -1202,72 +1202,74 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr let context = self.context let shareController = context.sharedContext.makeShareController( context: context, - subject: .url(link), - forceExternal: false, - shareStory: { [weak self] in - guard let self, let parentController = self.parentController else { - return - } - Queue.mainQueue().after(0.15) { - let controller = self.context.sharedContext.makeStorySharingScreen(context: self.context, subject: .gift(gift), parentController: parentController) - parentController.push(controller) - } - }, - enqueued: { [weak self] peerIds, _ in - let _ = (context.engine.data.get( - EngineDataList( - peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) + params: ShareControllerParams( + subject: .url(link), + externalShare: false, + actionCompleted: { [weak self] in + self?.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .current) + }, + enqueued: { [weak self] peerIds, _ in + let _ = (context.engine.data.get( + EngineDataList( + peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) + ) ) - ) - |> deliverOnMainQueue).startStandalone(next: { [weak self] peerList in + |> deliverOnMainQueue).startStandalone(next: { [weak self] peerList in + guard let self, let parentController = self.parentController else { + return + } + + let peers = peerList.compactMap { $0 } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let text: String + var savedMessages = false + if peerIds.count == 1, let peerId = peerIds.first, peerId == context.account.peerId { + text = presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One + savedMessages = true + } else { + if peers.count == 1, let peer = peers.first { + var peerName = peer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + var firstPeerName = firstPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "") + var secondPeerName = secondPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string + } else if let peer = peers.first { + var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string + } else { + text = "" + } + } + + parentController.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: true, animateInAsReplacement: false, action: { [weak self] action in + if savedMessages, action == .info { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let peer, let navigationController = self?.parentController?.navigationController as? NavigationController else { + return + } + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(peer), subject: nil, botStart: nil, updateTextInputState: nil, keepStack: .always, useExisting: true, purposefulAction: nil, scrollToEndIfExists: false, activateMessageSearch: nil, animated: true)) + }) + } + return false + }, additionalView: nil), in: .current) + }) + }, + shareStory: { [weak self] in guard let self, let parentController = self.parentController else { return } - - let peers = peerList.compactMap { $0 } - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let text: String - var savedMessages = false - if peerIds.count == 1, let peerId = peerIds.first, peerId == context.account.peerId { - text = presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One - savedMessages = true - } else { - if peers.count == 1, let peer = peers.first { - var peerName = peer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - peerName = peerName.replacingOccurrences(of: "**", with: "") - text = presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string - } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { - var firstPeerName = firstPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "") - var secondPeerName = secondPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "") - text = presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string - } else if let peer = peers.first { - var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - peerName = peerName.replacingOccurrences(of: "**", with: "") - text = presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string - } else { - text = "" - } + Queue.mainQueue().after(0.15) { + let controller = self.context.sharedContext.makeStorySharingScreen(context: self.context, subject: .gift(gift), parentController: parentController) + parentController.push(controller) } - - parentController.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: true, animateInAsReplacement: false, action: { [weak self] action in - if savedMessages, action == .info { - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) - |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let peer, let navigationController = self?.parentController?.navigationController as? NavigationController else { - return - } - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(peer), subject: nil, botStart: nil, updateTextInputState: nil, keepStack: .always, useExisting: true, purposefulAction: nil, scrollToEndIfExists: false, activateMessageSearch: nil, animated: true)) - }) - } - return false - }, additionalView: nil), in: .current) - }) - }, - actionCompleted: { [weak self] in - self?.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .current) - } + } + ) ) self.parentController?.present(shareController, in: .window(.root)) }) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 58f72fcab0..9f1663f763 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -31,7 +31,6 @@ import StoryContainerScreen import EmptyStateIndicatorComponent import UIKitRuntimeUtils import PeerInfoPaneNode -import ShareController import UndoUI import PlainButtonComponent import ComponentDisplayAdapters @@ -2771,24 +2770,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr return } - let shareController = ShareController( - context: self.context, + let shareController = self.context.sharedContext.makeShareController(context: self.context, params: ShareControllerParams( subject: .media(.story(peer: peerReference, id: item.id, media: TelegramMediaStory(storyId: StoryId(peerId: peer.id, id: item.id), isMention: false)), nil), - 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 - ) + externalShare: false + )) self.parentController?.present(shareController, in: .window(.root)) }) }) @@ -5404,83 +5389,69 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr urlBase = "\(peer.id.id._internalGetInt64Value())" } - let shareController = ShareController( - context: self.context, + let shareController = self.context.sharedContext.makeShareController(context: self.context, params: ShareControllerParams( subject: .url("https://t.me/\(urlBase)/a/\(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)) - shareController.completed = { [weak self] peerIds in - guard let self else { - return - } - let _ = (self.context.engine.data.get( - EngineDataList( - peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) - ) - ) - |> deliverOnMainQueue).startStandalone(next: { [weak self] peerList in + actionCompleted: { [weak self] in guard let self else { return } - - let peers = peerList.compactMap { $0 } let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - - let text: String - var savedMessages = false - if peers.count == 1, let peer = peers.first { - let peerName = peer.id == self.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - text = presentationData.strings.WebBrowser_LinkForwardTooltip_Chat_One(peerName).string - savedMessages = peer.id == self.context.account.peerId - } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { - let firstPeerName = firstPeer.id == self.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - let secondPeerName = secondPeer.id == self.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - text = presentationData.strings.WebBrowser_LinkForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string - } else if let peer = peers.first { - let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - text = presentationData.strings.WebBrowser_LinkForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string - } else { - text = "" + self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + }, + completed: { [weak self] peerIds in + guard let self else { + return } - - self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in - if savedMessages, let self, action == .info { - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) - |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let self, let peer else { - return - } - guard let navigationController = self.parentController?.navigationController as? NavigationController else { - return - } - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), forceOpenChat: true)) - }) + let _ = (self.context.engine.data.get( + EngineDataList( + peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) + ) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] peerList in + guard let self else { + return } - return false - }), in: .current) - }) - } - shareController.actionCompleted = { [weak self] in - guard let self else { - return + + let peers = peerList.compactMap { $0 } + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + let text: String + var savedMessages = false + if peers.count == 1, let peer = peers.first { + let peerName = peer.id == self.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.WebBrowser_LinkForwardTooltip_Chat_One(peerName).string + savedMessages = peer.id == self.context.account.peerId + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + let firstPeerName = firstPeer.id == self.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + let secondPeerName = secondPeer.id == self.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.WebBrowser_LinkForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string + } else if let peer = peers.first { + let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.WebBrowser_LinkForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string + } else { + text = "" + } + + self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in + if savedMessages, let self, action == .info { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.parentController?.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), forceOpenChat: true)) + }) + } + return false + }), in: .current) + }) } - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) - } + )) + self.parentController?.present(shareController, in: .window(.root)) } } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift index 53afd63f23..0158733e80 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift @@ -25,7 +25,7 @@ import ListItemComponentAdaptor import ButtonComponent import PlainButtonComponent import UndoUI -import ShareController + final class PostSuggestionsSettingsScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -200,8 +200,10 @@ final class PostSuggestionsSettingsScreenComponent: Component { } let context = component.context - let shareController = ShareController(context: context, subject: .url(link), updatedPresentationData: nil) - shareController.completed = { [weak controller] peerIds in + let shareController = context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: .url(link), updatedPresentationData: nil, actionCompleted: { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + controller.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + }, completed: { [weak controller] peerIds in let _ = (context.engine.data.get( EngineDataList( peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) @@ -210,7 +212,7 @@ final class PostSuggestionsSettingsScreenComponent: Component { |> deliverOnMainQueue).start(next: { [weak controller] peerList in let peers = peerList.compactMap { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 } - + let text: String var savedMessages = false if peerIds.count == 1, let peerId = peerIds.first, peerId == context.account.peerId { @@ -231,7 +233,7 @@ final class PostSuggestionsSettingsScreenComponent: Component { text = "" } } - + controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in if savedMessages, action == .info { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) @@ -248,11 +250,7 @@ final class PostSuggestionsSettingsScreenComponent: Component { return false }), in: .window(.root)) }) - } - shareController.actionCompleted = { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - controller.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) - } + })) controller.present(shareController, in: .window(.root)) } diff --git a/submodules/TelegramUI/Components/PeerReportScreen/Sources/ReportPeerDetailsActionSheetItem.swift b/submodules/TelegramUI/Components/PeerReportScreen/Sources/ReportPeerDetailsActionSheetItem.swift index 27aa262598..dd5f342bdc 100644 --- a/submodules/TelegramUI/Components/PeerReportScreen/Sources/ReportPeerDetailsActionSheetItem.swift +++ b/submodules/TelegramUI/Components/PeerReportScreen/Sources/ReportPeerDetailsActionSheetItem.swift @@ -5,8 +5,8 @@ import TelegramCore import TelegramPresentationData import TelegramUIPreferences import AccountContext -import ShareController import AppBundle +import ShareController public final class ReportPeerDetailsActionSheetItem: ActionSheetItem { let context: AccountContext diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift index 6d359ac7bc..2bfe32b1a7 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift @@ -18,7 +18,6 @@ import ListActionItemComponent import BundleIconComponent import TextFormat import UndoUI -import ShareController import ContextUI final class BusinessLinksSetupScreenComponent: Component { @@ -258,7 +257,7 @@ final class BusinessLinksSetupScreenComponent: Component { return } - environment.controller()?.present(ShareController(context: component.context, subject: .url(link.url), showInChat: nil, externalShare: false, immediateExternalShare: false), in: .window(.root)) + environment.controller()?.present(component.context.sharedContext.makeShareController(context: component.context, params: ShareControllerParams(subject: .url(link.url), showInChat: nil, externalShare: false, immediateExternalShare: false)), in: .window(.root)) } func update(component: BusinessLinksSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryController.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryController.swift index 9062918881..40883a64c5 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryController.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryController.swift @@ -11,7 +11,6 @@ import TelegramPresentationData import TelegramUIPreferences import MediaResources import AccountContext -import ShareController import GalleryUI import HexColor import CounterControllerTitleView @@ -1100,7 +1099,6 @@ public class WallpaperGalleryController: ViewController { return } - var controller: ShareController? var options: [String] = [] if (itemNode.options.contains(.blur)) { if (itemNode.options.contains(.motion)) { @@ -1111,8 +1109,13 @@ public class WallpaperGalleryController: ViewController { } else if (itemNode.options.contains(.motion)) { options.append("mode=motion") } - + let context = self.context + let actionCompleted: () -> Void = { [weak self] in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + } + switch wallpaper { case .image: let _ = (context.wallpaperUploadManager!.stateSignal() @@ -1125,12 +1128,8 @@ public class WallpaperGalleryController: ViewController { if !options.isEmpty { optionsString = "?\(options.joined(separator: "&"))" } - - let shareController = ShareController(context: context, subject: .url("https://t.me/bg/\(file.slug)\(optionsString)")) - shareController.actionCompleted = { [weak self] in - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) - } + + let shareController = context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: .url("https://t.me/bg/\(file.slug)\(optionsString)"), actionCompleted: actionCompleted)) self?.present(shareController, in: .window(.root), blockInteraction: true) } }) @@ -1159,15 +1158,17 @@ public class WallpaperGalleryController: ViewController { options.append("rotation=\(rotation)") } } - + var optionsString = "" if !options.isEmpty { optionsString = "?\(options.joined(separator: "&"))" } - - controller = ShareController(context: context, subject: .url("https://t.me/bg/\(file.slug)\(optionsString)")) + + let controller = context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: .url("https://t.me/bg/\(file.slug)\(optionsString)"), actionCompleted: actionCompleted)) + self.present(controller, in: .window(.root), blockInteraction: true) case let .color(color): - controller = ShareController(context: context, subject: .url("https://t.me/bg/\(UIColor(rgb: color).hexString)")) + let controller = context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: .url("https://t.me/bg/\(UIColor(rgb: color).hexString)"), actionCompleted: actionCompleted)) + self.present(controller, in: .window(.root), blockInteraction: true) case let .gradient(gradient): var colorsString = "" @@ -1182,16 +1183,10 @@ public class WallpaperGalleryController: ViewController { colorsString.append(UIColor(rgb: color).hexString) } - controller = ShareController(context: context, subject: .url("https://t.me/bg/\(colorsString)")) + let controller = context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: .url("https://t.me/bg/\(colorsString)"), actionCompleted: actionCompleted)) + self.present(controller, in: .window(.root), blockInteraction: true) default: break } - if let controller = controller { - controller.actionCompleted = { [weak self] in - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) - } - self.present(controller, in: .window(.root), blockInteraction: true) - } } } diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridController.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridController.swift index 55d0c49b87..1fda3c4cb0 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridController.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridController.swift @@ -10,7 +10,6 @@ import TelegramPresentationData import TelegramUIPreferences import OverlayStatusController import AccountContext -import ShareController import SearchUI import HexColor import PresentationDataUtils @@ -466,7 +465,7 @@ public final class ThemeGridController: ViewController { } else { subject = .text(string) } - let shareController = ShareController(context: context, subject: subject) + let shareController = context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: subject)) self.present(shareController, in: .window(.root), blockInteraction: true) self.donePressed() diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 68a5ca4adc..e48abd47d2 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -7,7 +7,6 @@ import AccountContext import SwiftSignalKit import AppBundle import MessageInputPanelComponent -import ShareController import TelegramCore import Postbox import UndoUI diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 1d9f246881..ed20ffc4e6 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -27,7 +27,6 @@ import StoryFooterPanelComponent import TelegramPresentationData import LegacyInstantVideoController import TelegramPresentationData -import ShareController import ChatPresentationInterfaceState import Postbox import OverlayStatusController @@ -54,6 +53,7 @@ import AnimatedTextComponent import ChatSendAsContextMenu import ShareWithPeersScreen import AlertComponent +import ShareController private var ObjCKey_DeinitWatcher: Int? @@ -68,7 +68,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { private var inputPanelExternalState: MessageInputPanelComponent.ExternalState? weak var attachmentController: AttachmentController? - weak var shareController: ShareController? + weak var shareController: ViewController? weak var tooltipScreen: ViewController? weak var actionSheet: ViewController? weak var statusController: ViewController? @@ -1323,101 +1323,99 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { })) } - let shareController = ShareController( - context: component.context, + let shareController = component.context.sharedContext.makeShareController(context: component.context, params: ShareControllerParams( subject: .media(AnyMediaReference.standalone(media: TelegramMediaStory(storyId: StoryId(peerId: peerId, id: focusedItem.storyItem.id), isMention: false)), nil), preferredAction: preferredAction ?? .default, externalShare: false, immediateExternalShare: false, - forceTheme: defaultDarkColorPresentationTheme - ) - shareController.shareStory = { [weak view] in - guard let view else { - return - } - view.openStoryEditing(repost: true) - } - shareController.completed = { [weak view] peerIds in - guard let view, let component = view.component else { - return - } - - let _ = (component.context.engine.data.get( - EngineDataList( - peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) - ) - ) - |> deliverOnMainQueue).start(next: { [weak view] peerList in + forceTheme: defaultDarkColorPresentationTheme, + dismissed: { [weak self, weak view] _ in + guard let self, let view else { + return + } + self.shareController = nil + view.updateIsProgressPaused() + }, + completed: { [weak view] peerIds in guard let view, let component = view.component else { return } - - let peers = peerList.compactMap { $0 } - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - let text: String - var savedMessages = false - if peerIds.count == 1, let peerId = peerIds.first, peerId == component.context.account.peerId { - text = presentationData.strings.Conversation_StoryForwardTooltip_SavedMessages_One - savedMessages = true - } else { - if peers.count == 1, let peer = peers.first { - var peerName = peer.id == component.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - peerName = peerName.replacingOccurrences(of: "**", with: "") - text = presentationData.strings.Conversation_StoryForwardTooltip_Chat_One(peerName).string - } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { - var firstPeerName = firstPeer.id == component.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "") - var secondPeerName = secondPeer.id == component.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "") - text = presentationData.strings.Conversation_StoryForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string - } else if let peer = peers.first { - var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - peerName = peerName.replacingOccurrences(of: "**", with: "") - text = presentationData.strings.Conversation_StoryForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string - } else { - text = "" + + let _ = (component.context.engine.data.get( + EngineDataList( + peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) + ) + ) + |> deliverOnMainQueue).start(next: { [weak view] peerList in + guard let view, let component = view.component else { + return } - } - - if let controller = component.controller() { - let context = component.context + + let peers = peerList.compactMap { $0 } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - controller.present(UndoOverlayController( - presentationData: presentationData, - content: .forward(savedMessages: savedMessages, text: text), - elevatedLayout: false, - animateInAsReplacement: false, - action: { [weak controller] action in - if savedMessages, action == .info { - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) - |> deliverOnMainQueue).start(next: { peer in - guard let controller, let peer else { - return - } - guard let navigationController = controller.navigationController as? NavigationController else { - return - } - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), forceOpenChat: true)) - }) - } - return false + let text: String + var savedMessages = false + if peerIds.count == 1, let peerId = peerIds.first, peerId == component.context.account.peerId { + text = presentationData.strings.Conversation_StoryForwardTooltip_SavedMessages_One + savedMessages = true + } else { + if peers.count == 1, let peer = peers.first { + var peerName = peer.id == component.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_StoryForwardTooltip_Chat_One(peerName).string + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + var firstPeerName = firstPeer.id == component.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "") + var secondPeerName = secondPeer.id == component.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_StoryForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string + } else if let peer = peers.first { + var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_StoryForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string + } else { + text = "" } - ), in: .current) + } + + if let controller = component.controller() { + let context = component.context + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + controller.present(UndoOverlayController( + presentationData: presentationData, + content: .forward(savedMessages: savedMessages, text: text), + elevatedLayout: false, + animateInAsReplacement: false, + action: { [weak controller] action in + if savedMessages, action == .info { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let controller, let peer else { + return + } + guard let navigationController = controller.navigationController as? NavigationController else { + return + } + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), forceOpenChat: true)) + }) + } + return false + } + ), in: .current) + } + }) + }, + shareStory: { [weak view] in + guard let view else { + return } - }) - } - + view.openStoryEditing(repost: true) + } + )) + self.shareController = shareController view.updateIsProgressPaused() - - shareController.dismissed = { [weak self, weak view] _ in - guard let self, let view else { - return - } - self.shareController = nil - view.updateIsProgressPaused() - } - + controller.present(shareController, in: .window(.root)) } } @@ -1501,19 +1499,17 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { let theme = component.theme let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) - let shareController = ShareController(context: component.context, subject: .text(text), externalShare: true, immediateExternalShare: false, updatedPresentationData: updatedPresentationData) - - self.shareController = shareController - view.updateIsProgressPaused() - - shareController.dismissed = { [weak self, weak view] _ in + let shareController = component.context.sharedContext.makeShareController(context: component.context, params: ShareControllerParams(subject: .text(text), externalShare: true, immediateExternalShare: false, updatedPresentationData: updatedPresentationData, dismissed: { [weak self, weak view] _ in guard let self, let view else { return } self.shareController = nil view.updateIsProgressPaused() - } - + })) + + self.shareController = shareController + view.updateIsProgressPaused() + controller.present(shareController, in: .window(.root)) } diff --git a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingTextAreaComponent.swift b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingTextAreaComponent.swift index 85677c79c7..a032da60e0 100644 --- a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingTextAreaComponent.swift +++ b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingTextAreaComponent.swift @@ -592,7 +592,7 @@ final class TextProcessingTextAreaComponent: Component { case .copy: storeAttributedTextInPasteboard(text) case .share: - let shareController = component.context.sharedContext.makeShareController(context: component.context, subject: .text(text.string), forceExternal: true, shareStory: nil, enqueued: nil, actionCompleted: nil) + let shareController = component.context.sharedContext.makeShareController(context: component.context, params: ShareControllerParams(subject: .text(text.string))) component.present(shareController, nil) case .lookup: let controller = UIReferenceLibraryViewController(term: text.string) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 1a5ff0c4bb..748ae33859 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -18,7 +18,6 @@ import AccountContext import TelegramStringFormatting import OverlayStatusController import DeviceLocationManager -import ShareController import UrlEscaping import ContextUI import AlertUI @@ -2082,9 +2081,9 @@ extension ChatControllerImpl { if let strongSelf = self, !messages.isEmpty { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) - let shareController = ShareController(context: strongSelf.context, subject: .messages(messages.sorted(by: { lhs, rhs in + let shareController = strongSelf.context.sharedContext.makeShareController(context: strongSelf.context, params: ShareControllerParams(subject: .messages(messages.sorted(by: { lhs, rhs in return lhs.index < rhs.index - }).map { $0._asMessage() }), externalShare: true, immediateExternalShare: true, updatedPresentationData: strongSelf.updatedPresentationData) + }).map { $0._asMessage() }), externalShare: true, immediateExternalShare: true, updatedPresentationData: strongSelf.updatedPresentationData)) strongSelf.chatDisplayNode.dismissInput() strongSelf.present(shareController, in: .window(.root)) } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift index 89b1b723d9..662ce26d34 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift @@ -18,7 +18,6 @@ import AccountContext import TelegramStringFormatting import OverlayStatusController import DeviceLocationManager -import ShareController import UrlEscaping import ContextUI import AlertUI diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift index b08831c0d5..c2d0e1bb68 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift @@ -18,7 +18,6 @@ import AccountContext import TelegramStringFormatting import OverlayStatusController import DeviceLocationManager -import ShareController import UrlEscaping import ContextUI import AlertUI diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift index 0aa529ccfb..4a364bda5a 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift @@ -18,7 +18,6 @@ import AccountContext import TelegramStringFormatting import OverlayStatusController import DeviceLocationManager -import ShareController import UrlEscaping import ContextUI import AlertUI diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenViewOnceMediaMessage.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenViewOnceMediaMessage.swift index f23de4f234..ec6308a9f7 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenViewOnceMediaMessage.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenViewOnceMediaMessage.swift @@ -18,7 +18,6 @@ import AccountContext import TelegramStringFormatting import OverlayStatusController import DeviceLocationManager -import ShareController import UrlEscaping import ContextUI import AlertUI diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerReport.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerReport.swift index 2f485b8010..a08513ec6c 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerReport.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerReport.swift @@ -18,7 +18,6 @@ import AccountContext import TelegramStringFormatting import OverlayStatusController import DeviceLocationManager -import ShareController import UrlEscaping import ContextUI import AlertUI diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerThemeManagement.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerThemeManagement.swift index a444092c18..d3665dee65 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerThemeManagement.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerThemeManagement.swift @@ -18,7 +18,6 @@ import AccountContext import TelegramStringFormatting import OverlayStatusController import DeviceLocationManager -import ShareController import UrlEscaping import ContextUI import AlertUI diff --git a/submodules/TelegramUI/Sources/ChatBusinessLinkTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatBusinessLinkTitlePanelNode.swift index fd0e74da2c..16b61ed7d2 100644 --- a/submodules/TelegramUI/Sources/ChatBusinessLinkTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatBusinessLinkTitlePanelNode.swift @@ -13,7 +13,6 @@ import AccountContext import TelegramCore import SwiftSignalKit import UndoUI -import ShareController import LegacyChatHeaderPanelComponent private final class ChatBusinessLinkTitlePanelComponent: Component { @@ -184,7 +183,7 @@ final class ChatBusinessLinkTitlePanelNode: ChatTitleAccessoryPanelNode { return } - interfaceInteraction.presentController(ShareController(context: self.context, subject: .url(link.url), showInChat: nil, externalShare: false, immediateExternalShare: false), nil) + interfaceInteraction.presentController(self.context.sharedContext.makeShareController(context: self.context, params: ShareControllerParams(subject: .url(link.url), showInChat: nil, externalShare: false, immediateExternalShare: false)), nil) } override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> LayoutResult { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index b7293406a9..85839d3756 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -18,7 +18,6 @@ import AccountContext import TelegramStringFormatting import OverlayStatusController import DeviceLocationManager -import ShareController import UrlEscaping import ContextUI import AlertUI @@ -3995,7 +3994,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let self else { return } - let shareController = ShareController(context: self.context, subject: .text(text.string), externalShare: true, immediateExternalShare: false, updatedPresentationData: self.updatedPresentationData) + let shareController = self.context.sharedContext.makeShareController(context: self.context, params: ShareControllerParams(subject: .text(text.string), externalShare: true, immediateExternalShare: false, updatedPresentationData: self.updatedPresentationData)) self.chatDisplayNode.dismissInput() self.present(shareController, in: .window(.root)) } diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 871545bc9b..3b4906d18d 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -888,7 +888,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self?.interfaceInteraction?.presentController(controller, nil) }) if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["ios_disable_ai_chat"] as? Double, value == 1.0 { - } else if let peerId = self.presentationInterfaceState.chatLocation.peerId, peerId.namespace != Namespaces.Peer.SecretChat { + } else if let peerId = self.chatPresentationInterfaceState.chatLocation.peerId, peerId.namespace != Namespaces.Peer.SecretChat { self.textInputPanelNode?.isAIEnabled = true } self.textInputPanelNode?.textInputAccessoryPanel = textInputAccessoryPanel diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index bf77a22fe9..e8ba9b0947 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -12,6 +12,7 @@ import PresentationDataUtils import TextFormat import UrlHandling import AccountContext +import ShareController import ChatPresentationInterfaceState import LegacyComponents import LegacyUI @@ -32,7 +33,6 @@ import TelegramCallsUI import AutomaticBusinessMessageSetupScreen import MediaEditorScreen import CameraScreen -import ShareController import ComposeTodoScreen import ComposePollScreen import Photos diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift index db0483902e..ea565b6fe6 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift @@ -9,7 +9,6 @@ import ChatControllerInteraction import Display import UIKit import UndoUI -import ShareController import ChatShareMessageTagView import ReactionSelectionNode import TopMessageReactions @@ -109,9 +108,6 @@ extension ChatControllerImpl { warnAboutPrivate = true } } - let shareController = ShareController(context: self.context, subject: .messages(messages), updatedPresentationData: self.updatedPresentationData, shareAsLink: true) - shareController.parentNavigationController = self.navigationController as? NavigationController - if let message = messages.first, message.media.contains(where: { media in if media is TelegramMediaContact || media is TelegramMediaPoll || media is TelegramMediaTodo { return true @@ -126,24 +122,18 @@ extension ChatControllerImpl { if message.text.containsOnlyEmoji { canShareToStory = false } - - if canShareToStory { - shareController.shareStory = { [weak self] in - guard let self else { - return - } - Queue.mainQueue().after(0.15) { - let controller = self.context.sharedContext.makeStorySharingScreen(context: self.context, subject: .messages(messages), parentController: self) - self.push(controller) - } + + let shareStory: (() -> Void)? = canShareToStory ? { [weak self] in + guard let self else { + return } - } - shareController.dismissed = { [weak self] shared in - if shared { - self?.commitPurposefulAction() + Queue.mainQueue().after(0.15) { + let controller = self.context.sharedContext.makeStorySharingScreen(context: self.context, subject: .messages(messages), parentController: self) + self.push(controller) } - } - shareController.actionCompleted = { [weak self] in + } : nil + + let shareController = self.context.sharedContext.makeShareController(context: self.context, params: ShareControllerParams(subject: .messages(messages), updatedPresentationData: self.updatedPresentationData, shareAsLink: true, actionCompleted: { [weak self] in guard let self else { return } @@ -154,12 +144,15 @@ extension ChatControllerImpl { content = .linkCopied(title: nil, text: self.presentationData.strings.Conversation_LinkCopied) } self.present(UndoOverlayController(presentationData: self.presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - } - shareController.enqueued = { [weak self] peerIds, correlationIds in + }, dismissed: { [weak self] shared in + if shared { + self?.commitPurposefulAction() + } + }, enqueued: { [weak self] peerIds, correlationIds in guard let self else { return } - + let _ = (self.context.engine.data.get( EngineDataList( peerIds.map(TelegramEngine.EngineData.Item.Peer.RenderedPeer.init) @@ -195,20 +188,20 @@ extension ChatControllerImpl { text = "" } } - + let reactionItems: Signal<[ReactionItem], NoError> if savedMessages { reactionItems = tagMessageReactions(context: self.context, subPeerId: self.chatLocation.threadId.flatMap(EnginePeer.Id.init)) } else { reactionItems = .single([]) } - + let _ = (reactionItems |> deliverOnMainQueue).startStandalone(next: { [weak self] reactionItems in guard let self else { return } - + self.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, position: savedMessages ? .top : .bottom, animateInAsReplacement: !savedMessages, action: { [weak self] action in if savedMessages, let self, action == .info { let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) @@ -226,7 +219,7 @@ extension ChatControllerImpl { }, additionalView: savedMessages ? chatShareToSavedMessagesAdditionalView(self, reactionItems: reactionItems, correlationIds: correlationIds) : nil), in: .current) }) }) - } + }, shareStory: shareStory, parentNavigationController: self.navigationController as? NavigationController)) self.chatDisplayNode.dismissInput() self.present(shareController, in: .window(.root), blockInteraction: true) } diff --git a/submodules/TelegramUI/Sources/ComposeController.swift b/submodules/TelegramUI/Sources/ComposeController.swift index d4825816b6..0d1a5a16fd 100644 --- a/submodules/TelegramUI/Sources/ComposeController.swift +++ b/submodules/TelegramUI/Sources/ComposeController.swift @@ -7,13 +7,13 @@ import SwiftSignalKit import TelegramCore import TelegramPresentationData import AccountContext +import ShareController import AlertUI import PresentationDataUtils import SearchUI import TelegramPermissionsUI import AppBundle import DeviceAccess -import ShareController public class ComposeControllerImpl: ViewController, ComposeController { private let context: AccountContext diff --git a/submodules/TelegramUI/Sources/OpenAddContact.swift b/submodules/TelegramUI/Sources/OpenAddContact.swift index fabba71aaf..2a32e11323 100644 --- a/submodules/TelegramUI/Sources/OpenAddContact.swift +++ b/submodules/TelegramUI/Sources/OpenAddContact.swift @@ -4,10 +4,10 @@ import TelegramCore import Display import DeviceAccess import AccountContext +import ShareController import AlertUI import PresentationDataUtils import PeerInfoUI -import ShareController func openAddContactImpl(context: AccountContext, firstName: String = "", lastName: String = "", phoneNumber: String, label: String = "_$!!$_", present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, completed: @escaping () -> Void = {}) { let _ = (DeviceAccess.authorizationStatus(subject: .contacts) diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index a8442864c2..968abcb521 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -9,6 +9,7 @@ import Lottie import TelegramUIPreferences import TelegramPresentationData import AccountContext +import ShareController import GalleryUI import InstantPageUI import LocationUI @@ -18,7 +19,6 @@ import PeerInfoUI import SettingsUI import AlertUI import PresentationDataUtils -import ShareController import UndoUI import WebsiteType import GalleryData @@ -233,7 +233,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { params.dismissInput() let presentationData = params.context.sharedContext.currentPresentationData.with { $0 } if immediateShare { - let controller = ShareController(context: params.context, subject: .media(.standalone(media: file), nil), immediateExternalShare: true) + let controller = params.context.sharedContext.makeShareController(context: params.context, params: ShareControllerParams(subject: .media(.standalone(media: file), nil), immediateExternalShare: true)) params.present(controller, nil, .window(.root)) } else if let rootController = params.navigationController?.view.window?.rootViewController { let proceed = { diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 696fc1daa7..e3c040419d 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -20,7 +20,6 @@ import JoinLinkPreviewUI import LanguageLinkPreviewUI import SettingsUI import UrlHandling -import ShareController import ChatInterfaceState import TelegramCallsUI import UndoUI @@ -624,10 +623,9 @@ func openResolvedUrlImpl( } } else { if let url = url, !url.isEmpty { - let shareController = ShareController(context: context, subject: .url(url), presetText: text, externalShare: false, immediateExternalShare: false) - shareController.actionCompleted = { + let shareController = context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: .url(url), presetText: text, externalShare: false, immediateExternalShare: false, actionCompleted: { present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) - } + })) present(shareController, nil) context.sharedContext.applicationBindings.dismissNativeController() } else { diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift index bce9e5b293..6240209a75 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift @@ -6,7 +6,6 @@ import Display import SwiftSignalKit import TelegramUIPreferences import AccountContext -import ShareController import UndoUI import AttachmentFileController import LegacyMediaPickerUI @@ -82,13 +81,12 @@ final class OverlayAudioPlayerControllerImpl: ViewController, OverlayAudioPlayer if case .messages = subject { canShowInChat = true } - let shareController = ShareController(context: strongSelf.context, subject: subject, showInChat: canShowInChat ? { message in + let shareController = strongSelf.context.sharedContext.makeShareController(context: strongSelf.context, params: ShareControllerParams(subject: subject, showInChat: canShowInChat ? { message in if let strongSelf = self { strongSelf.context.sharedContext.navigateToChat(accountId: strongSelf.context.account.id, peerId: message.id.peerId, messageId: message.id) strongSelf.dismiss() } - } : nil, externalShare: true) - shareController.completed = { [weak self] peerIds in + } : nil, externalShare: true, completed: { [weak self] peerIds in if let strongSelf = self { let _ = (strongSelf.context.engine.data.get( EngineDataList( @@ -99,7 +97,7 @@ final class OverlayAudioPlayerControllerImpl: ViewController, OverlayAudioPlayer if let strongSelf = self { let peers = peerList.compactMap { $0 } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - + let text: String var savedMessages = false if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId { @@ -124,7 +122,7 @@ final class OverlayAudioPlayerControllerImpl: ViewController, OverlayAudioPlayer text = "" } } - + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in if savedMessages, let self, action == .info { let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) @@ -143,7 +141,7 @@ final class OverlayAudioPlayerControllerImpl: ViewController, OverlayAudioPlayer } }) } - } + })) strongSelf.controllerNode.view.endEditing(true) strongSelf.present(shareController, in: .window(.root)) } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 450fe8033b..85a08a8197 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -4223,11 +4223,17 @@ public final class SharedAccountContextImpl: SharedAccountContext { }) } - public func makeShareController(context: AccountContext, subject: ShareControllerSubject, forceExternal: Bool, shareStory: (() -> Void)?, enqueued: (([PeerId], [Int64]) -> Void)?, actionCompleted: (() -> Void)?) -> ViewController { - let controller = ShareController(context: context, subject: subject, externalShare: forceExternal) - controller.shareStory = shareStory - controller.enqueued = enqueued - controller.actionCompleted = actionCompleted + public func makeShareController(context: AccountContext, params: ShareControllerParams) -> ViewController { + let controller = ShareController(context: context, subject: params.subject, presetText: params.presetText, preferredAction: params.preferredAction, showInChat: params.showInChat, fromForeignApp: params.fromForeignApp, segmentedValues: params.segmentedValues, externalShare: params.externalShare, immediateExternalShare: params.immediateExternalShare, immediatePeerId: params.immediatePeerId, updatedPresentationData: params.updatedPresentationData, forceTheme: params.forceTheme, forcedActionTitle: params.forcedActionTitle, shareAsLink: params.shareAsLink, collectibleItemInfo: params.collectibleItemInfo) + controller.actionCompleted = params.actionCompleted + controller.dismissed = params.dismissed + controller.completed = params.completed + controller.enqueued = params.enqueued + controller.shareStory = params.shareStory + controller.debugAction = params.debugAction + controller.onMediaTimestampLinkCopied = params.onMediaTimestampLinkCopied + controller.parentNavigationController = params.parentNavigationController + controller.canSendInHighQuality = params.canSendInHighQuality return controller } diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 7c217e478c..a20596acf5 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -27,7 +27,6 @@ import InstantPageUI import InstantPageCache import LocalAuth import OpenInExternalAppUI -import ShareController import UndoUI import AvatarNode import OverlayStatusController @@ -3940,11 +3939,10 @@ public final class WebAppController: ViewController, AttachmentContainable { guard let self else { return } - let shareController = ShareController(context: context, subject: .url("https://t.me/\(addressName)?profile")) - shareController.actionCompleted = { [weak self] in + let shareController = context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: .url("https://t.me/\(addressName)?profile"), actionCompleted: { [weak self] in let presentationData = context.sharedContext.currentPresentationData.with { $0 } self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) - } + })) self.present(shareController, in: .window(.root)) }))) } From 648430e37d552a0212fb78326b5356b06ce8e8be Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 3 Apr 2026 22:15:42 +0800 Subject: [PATCH 20/50] Various improvements --- .../Sources/AccountContext.swift | 4 +- .../Sources/ChatListController.swift | 2 +- .../Sources/ChatListSearchContainerNode.swift | 2 +- .../Sources/Node/ChatListItem.swift | 2 +- .../Sources/ContactsController.swift | 4 +- .../ChatItemGalleryFooterContentNode.swift | 22 +++- .../ChatVideoGalleryItemScrubberView.swift | 82 +++++++------- .../GalleryUI/Sources/GalleryController.swift | 15 ++- .../Sources/DeviceContactInfoController.swift | 18 ++- .../Sources/UserInfoController.swift | 2 + .../Sources/ManagedAudioSession.swift | 22 ++++ ...ChatInlineSearchResultsListComponent.swift | 51 ++++++++- .../Sources/ChatMessageBubbleItemNode.swift | 11 +- .../Sources/ChatMessageItemView.swift | 4 +- .../ChatMessageTextBubbleContentNode.swift | 11 +- .../Sources/ChatControllerInteraction.swift | 4 +- .../Sources/NewContactScreen.swift | 25 +++-- .../Sources/PhoneInputItem.swift | 4 +- .../Sources/InteractiveTextComponent.swift | 103 +++++++++++++++++- .../Sources/PeerInfoScreenOpenMessage.swift | 2 +- .../ChatControllerOpenLinkContextMenu.swift | 2 +- .../ChatControllerOpenPhoneContextMenu.swift | 2 +- .../TelegramUI/Sources/ChatController.swift | 20 ++-- .../Sources/ChatControllerNode.swift | 18 +++ .../ChatPinnedMessageTitlePanelNode.swift | 10 +- .../Sources/ComposeController.swift | 2 + .../TelegramUI/Sources/OpenAddContact.swift | 9 +- .../TelegramUI/Sources/OpenChatMessage.swift | 41 +++++++ .../TelegramUI/Sources/OpenResolvedUrl.swift | 2 + .../Sources/SharedAccountContext.swift | 8 +- 30 files changed, 396 insertions(+), 108 deletions(-) diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 3d5b79b61a..356a456045 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1445,7 +1445,7 @@ public protocol SharedAccountContext: AnyObject { func resolveUrl(context: AccountContext, peerId: PeerId?, url: String, skipUrlAuth: Bool) -> Signal func resolveUrlWithProgress(context: AccountContext, peerId: PeerId?, url: String, skipUrlAuth: Bool) -> Signal func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, forceUpdate: Bool, openPeer: @escaping (EnginePeer, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, UIView?, CGRect?) -> Bool)?, sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?, progress: Promise?, completion: (() -> Void)?) - func openAddContact(context: AccountContext, firstName: String, lastName: String, phoneNumber: String, label: String, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, completed: @escaping () -> Void) + func openAddContact(context: AccountContext, peer: EnginePeer?, firstName: String, lastName: String, phoneNumber: String, label: String, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, completed: @escaping () -> Void) func openAddPersonContact(context: AccountContext, peerId: PeerId, pushController: @escaping (ViewController) -> Void, present: @escaping (ViewController, Any?) -> Void) func presentContactsWarningSuppression(context: AccountContext, present: (ViewController, Any?) -> Void) func openImagePicker(context: AccountContext, completion: @escaping (UIImage) -> Void, present: @escaping (ViewController) -> Void) @@ -1550,7 +1550,7 @@ public protocol SharedAccountContext: AnyObject { func openCreateGroupCallUI(context: AccountContext, peerIds: [EnginePeer.Id], parentController: ViewController) - func makeNewContactScreen(context: AccountContext, peer: EnginePeer?, phoneNumber: String?, shareViaException: Bool, completion: @escaping (EnginePeer?, DeviceContactStableId?, DeviceContactExtendedData?) -> Void) -> ViewController + func makeNewContactScreen(context: AccountContext, peer: EnginePeer?, firstName: String?, lastName: String?, phoneNumber: String?, shareViaException: Bool, completion: @escaping (EnginePeer?, DeviceContactStableId?, DeviceContactExtendedData?) -> Void) -> ViewController func makeLoginEmailSetupController(context: AccountContext, blocking: Bool, emailPattern: String?, canAutoDismissIfNeeded: Bool, navigationController: NavigationController?, completion: @escaping () -> Void, dismiss: @escaping () -> Void) -> ViewController func makePasskeySetupController(context: AccountContext, displaySkip: Bool, navigationController: NavigationController?, completion: @escaping () -> Void, dismiss: @escaping () -> Void) -> ViewController diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 0debd0179e..b605a693a8 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1789,7 +1789,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.chatListDisplayNode.requestAddContact = { [weak self] phoneNumber in if let strongSelf = self { strongSelf.view.endEditing(true) - strongSelf.context.sharedContext.openAddContact(context: strongSelf.context, firstName: "", lastName: "", phoneNumber: phoneNumber, label: defaultContactLabel, present: { [weak self] controller, arguments in + strongSelf.context.sharedContext.openAddContact(context: strongSelf.context, peer: nil, firstName: "", lastName: "", phoneNumber: phoneNumber, label: defaultContactLabel, present: { [weak self] controller, arguments in self?.present(controller, in: .window(.root), with: arguments) }, pushController: { [weak self] controller in (self?.navigationController as? NavigationController)?.pushViewController(controller) diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index abbe656e9a..aef3f58aa0 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -1343,8 +1343,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } public override func searchTextClearTokens() { + self.folder = nil self.updateSearchOptions(nil) -// self.setQuery?(nil, [], self.searchQueryValue ?? "") } func deleteMessages(messageIds: Set?) { diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index ec2a65f95a..76692de347 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -1309,7 +1309,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } } - var item: ChatListItem? + public private(set) var item: ChatListItem? private let backgroundNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index be63c37d30..f8fbf6c94e 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -365,7 +365,7 @@ public class ContactsController: ViewController { self.contactsNode.requestAddContact = { [weak self] phoneNumber in if let strongSelf = self { strongSelf.view.endEditing(true) - strongSelf.context.sharedContext.openAddContact(context: strongSelf.context, firstName: "", lastName: "", phoneNumber: phoneNumber, label: defaultContactLabel, present: { [weak self] controller, arguments in + strongSelf.context.sharedContext.openAddContact(context: strongSelf.context, peer: nil, firstName: "", lastName: "", phoneNumber: phoneNumber, label: defaultContactLabel, present: { [weak self] controller, arguments in self?.present(controller, in: .window(.root), with: arguments) }, pushController: { [weak self] controller in (self?.navigationController as? NavigationController)?.pushViewController(controller) @@ -757,6 +757,8 @@ public class ContactsController: ViewController { let controller = strongSelf.context.sharedContext.makeNewContactScreen( context: strongSelf.context, peer: nil, + firstName: nil, + lastName: nil, phoneNumber: nil, shareViaException: false, completion: { [weak self] peer, stableId, contactData in diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index e6dab339fc..1b5551a902 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -310,8 +310,20 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } } didSet { - if let scrubberView = self.scrubberView { + if let scrubberView = self.scrubberView { self.view.addSubview(scrubberView) + scrubberView.onRequestLayout = { [weak self] transition in + guard let self else { + return + } + if let requestLayout = self.requestLayout { + requestLayout(transition) + } else { + if let validLayout = self.validLayout { + let _ = self.updateLayout(size: validLayout.0, metrics: validLayout.1, leftInset: validLayout.2, rightInset: validLayout.3, bottomInset: validLayout.4, contentInset: validLayout.5, transition: transition) + } + } + } scrubberView.updateScrubbingVisual = { [weak self] value in guard let strongSelf = self else { return @@ -1296,7 +1308,13 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll panelHeight -= 44.0 } - let scrubberFrame = CGRect(origin: CGPoint(x: buttonPanelInsets.left, y: scrubberY), size: CGSize(width: width - buttonPanelInsets.left - buttonPanelInsets.right, height: 44.0)) + var scrubberFrame = CGRect(origin: CGPoint(x: buttonPanelInsets.left, y: scrubberY), size: CGSize(width: width - buttonPanelInsets.left - buttonPanelInsets.right, height: 44.0)) + if scrubberView.hasVisibleInfo { + let infoHeight: CGFloat = 16.0 + scrubberFrame.size.height += infoHeight + panelHeight += infoHeight + } + scrubberView.updateLayout(size: scrubberFrame.size, leftInset: 0.0, rightInset: 0.0, isCollapsed: self.visibilityAlpha < 1.0, transition: transition) transition.updateBounds(layer: scrubberView.layer, bounds: CGRect(origin: CGPoint(), size: scrubberFrame.size)) transition.updatePosition(layer: scrubberView.layer, position: CGPoint(x: scrubberFrame.midX, y: scrubberFrame.midY)) diff --git a/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift b/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift index af13b1fc83..58f740c35b 100644 --- a/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift +++ b/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift @@ -38,13 +38,11 @@ final class ChatVideoGalleryItemScrubberView: UIView { private var chapters: [MediaPlayerScrubbingChapter] = [] private var fetchStatusDisposable = MetaDisposable() - private var scrubbingDisposable = MetaDisposable() private var chapterDisposable = MetaDisposable() private var loadingDisposable = MetaDisposable() private var leftTimestampNodePushed = false private var rightTimestampNodePushed = false - private var infoNodePushed = false private var currentChapter: MediaPlayerScrubbingChapter? @@ -53,6 +51,14 @@ final class ChatVideoGalleryItemScrubberView: UIView { private var currentLeftString: String? private var currentRightString: String? + var hasVisibleInfo: Bool { + if let attributedText = self.infoNode.attributedText, !attributedText.string.isEmpty { + return true + } else { + return false + } + } + var hideWhenDurationIsUnknown = false { didSet { if self.hideWhenDurationIsUnknown { @@ -72,6 +78,8 @@ final class ChatVideoGalleryItemScrubberView: UIView { var updateScrubbingHandlePosition: (CGFloat) -> Void = { _ in } var seek: (Double) -> Void = { _ in } + var onRequestLayout: ((ContainedViewLayoutTransition) -> Void)? + init(chapters: [MediaPlayerScrubbingChapter]) { self.backgroundContainer = GlassBackgroundContainerView() self.backgroundView = GlassBackgroundView() @@ -131,7 +139,7 @@ final class ChatVideoGalleryItemScrubberView: UIView { self.backgroundView.contentView.addSubview(self.leftTimestampNode.view) self.backgroundView.contentView.addSubview(self.rightTimestampNode.view) self.addSubview(self.scrubberNode.view) - //self.backgroundView.contentView.addSubview(self.infoNode.view) + self.backgroundView.contentView.addSubview(self.infoNode.view) } required init?(coder aDecoder: NSCoder) { @@ -139,7 +147,6 @@ final class ChatVideoGalleryItemScrubberView: UIView { } deinit { - self.scrubbingDisposable.dispose() self.fetchStatusDisposable.dispose() self.chapterDisposable.dispose() self.loadingDisposable.dispose() @@ -148,6 +155,16 @@ final class ChatVideoGalleryItemScrubberView: UIView { var isLoading = false var isCollapsed: Bool? + private func requestLayout(transition: ContainedViewLayoutTransition) { + if let onRequestLayout = self.onRequestLayout { + onRequestLayout(transition) + } else { + if let (size, leftInset, rightInset, isCollapsed) = self.containerLayout { + self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, isCollapsed: isCollapsed, transition: .immediate) + } + } + } + func updateTimestampsVisibility(animated: Bool) { if self.isAnimatedOut { return @@ -276,41 +293,11 @@ final class ChatVideoGalleryItemScrubberView: UIView { } strongSelf.infoNode.attributedText = NSAttributedString(string: chapter.title, font: textFont, textColor: .white) - if let (size, leftInset, rightInset, isCollapsed) = strongSelf.containerLayout { - strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, isCollapsed: isCollapsed, transition: .immediate) - } + strongSelf.requestLayout(transition: .immediate) } } })) } - - self.scrubbingDisposable.set((self.scrubberNode.scrubbingPosition - |> deliverOnMainQueue).start(next: { [weak self] value in - guard let strongSelf = self else { - return - } - let leftTimestampNodePushed: Bool = false - let rightTimestampNodePushed: Bool = false - let infoNodePushed: Bool - if let value = value { - //leftTimestampNodePushed = value < 0.16 - //rightTimestampNodePushed = value > 0.84 - infoNodePushed = value >= 0.16 && value <= 0.84 - } else { - //leftTimestampNodePushed = false - //rightTimestampNodePushed = false - infoNodePushed = false - } - if leftTimestampNodePushed != strongSelf.leftTimestampNodePushed || rightTimestampNodePushed != strongSelf.rightTimestampNodePushed || infoNodePushed != strongSelf.infoNodePushed { - strongSelf.leftTimestampNodePushed = leftTimestampNodePushed - strongSelf.rightTimestampNodePushed = rightTimestampNodePushed - strongSelf.infoNodePushed = infoNodePushed - - if let layout = strongSelf.containerLayout { - strongSelf.updateLayout(size: layout.0, leftInset: layout.1, rightInset: layout.2, isCollapsed: layout.3, transition: .animated(duration: 0.35, curve: .spring)) - } - } - })) } func setBufferingStatusSignal(_ status: Signal<(RangeSet, Int64)?, NoError>?) { @@ -335,16 +322,16 @@ final class ChatVideoGalleryItemScrubberView: UIView { } strongSelf.infoNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: .white) - if let (size, leftInset, rightInset, isCollapsed) = strongSelf.containerLayout { - strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, isCollapsed: isCollapsed, transition: .immediate) - } + strongSelf.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) } })) } else if self.chapters.isEmpty { self.infoNode.attributedText = NSAttributedString(string: dataSizeString(fileSize, forceDecimal: true, formatting: formatting), font: textFont, textColor: .white) + self.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) } } else if self.chapters.isEmpty { self.infoNode.attributedText = nil + self.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) } } @@ -376,27 +363,32 @@ final class ChatVideoGalleryItemScrubberView: UIView { } } + var yOffset: CGFloat = 0.0 + if !isCollapsed { + yOffset = size.height - 44.0 + } + leftTimestampOffset = 14.0 rightTimestampOffset = 14.0 - infoOffset = 0.0 + infoOffset = -8.0 if isCollapsed { scrubberLeftInset = 0.0 scrubberRightInset = 0.0 } - transition.setFrame(view: self.leftTimestampNode.view, frame: CGRect(origin: CGPoint(x: 16.0, y: leftTimestampOffset), size: CGSize(width: 60.0, height: 20.0))) - transition.setFrame(view: self.rightTimestampNode.view, frame: CGRect(origin: CGPoint(x: size.width - leftInset - rightInset - 60.0 - 16.0, y: rightTimestampOffset), size: CGSize(width: 60.0, height: 20.0))) + transition.setFrame(view: self.leftTimestampNode.view, frame: CGRect(origin: CGPoint(x: 16.0, y: yOffset + leftTimestampOffset), size: CGSize(width: 60.0, height: 20.0))) + transition.setFrame(view: self.rightTimestampNode.view, frame: CGRect(origin: CGPoint(x: size.width - leftInset - rightInset - 60.0 - 16.0, y: yOffset + rightTimestampOffset), size: CGSize(width: 60.0, height: 20.0))) var infoConstrainedSize = size - infoConstrainedSize.width = size.width - scrubberLeftInset - scrubberRightInset - 100.0 + infoConstrainedSize.width = size.width - 20.0 * 2.0 let infoSize = self.infoNode.measure(infoConstrainedSize) self.infoNode.bounds = CGRect(origin: CGPoint(), size: infoSize) - transition.setPosition(view: self.infoNode.view, position: CGPoint(x: size.width / 2.0, y: infoOffset + infoSize.height / 2.0)) - self.infoNode.alpha = size.width < size.height && self.isCollapsed == false ? 1.0 : 0.0 + transition.setPosition(view: self.infoNode.view, position: CGPoint(x: size.width / 2.0, y: yOffset + infoOffset + infoSize.height / 2.0)) + self.infoNode.alpha = self.isCollapsed == false ? 1.0 : 0.0 - var scrubberFrame = CGRect(origin: CGPoint(x: scrubberLeftInset, y: 15.0), size: CGSize(width: size.width - leftInset - rightInset - scrubberLeftInset - scrubberRightInset, height: scrubberHeight)) + var scrubberFrame = CGRect(origin: CGPoint(x: scrubberLeftInset, y: yOffset + 15.0), size: CGSize(width: size.width - leftInset - rightInset - scrubberLeftInset - scrubberRightInset, height: scrubberHeight)) if isCollapsed { scrubberFrame.origin.y = size.height - scrubberHeight } diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index b8cb27c13d..769144c9a8 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -927,8 +927,9 @@ public class GalleryController: ViewController, StandalonePresentableController, if case .custom = source { displayInfoOnTop = true } - + let syncResult = Atomic<(Bool, (() -> Void)?)>(value: (false, nil)) + var isFirstTime = true self.disposable.set(combineLatest( messageView, self.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) |> take(1), @@ -1015,14 +1016,22 @@ public class GalleryController: ViewController, StandalonePresentableController, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _, _ in }, generateStoreAfterDownload: strongSelf.generateStoreAfterDownload, sendSticker: strongSelf.actionInteraction?.sendSticker, - present: { [weak self] c, a in - if let strongSelf = self { + present: { [weak strongSelf] c, a in + if let strongSelf { strongSelf.presentInGlobalOverlay(c, with: a) } } ) { if isCentral { centralItemIndex = items.count + + if isFirstTime { + isFirstTime = false + if item is UniversalVideoGalleryItem { + } else { + strongSelf.galleryNode.areControlsHidden = false + } + } } items.append(item) } diff --git a/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift b/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift index 6b83cecb5d..75985d1823 100644 --- a/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift +++ b/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift @@ -1516,8 +1516,22 @@ public func pushContactContextOptionsController(context: AccountContext, context .action(ContextMenuActionItem(text: presentationData.strings.Chat_Context_Phone_CreateNewContact, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.contextMenu.primaryColor) }, action: { _, f in f(.default) - push(context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: context), environment: ShareControllerAppEnvironment(sharedContext: context.sharedContext), subject: .create(peer: peer?._asPeer(), contactData: contactData, isSharing: peer != nil, shareViaException: false, completion: { _, _, _ in - }), completed: nil, cancelled: nil)) + context.sharedContext.openAddContact( + context: context, + peer: peer, + firstName: contactData.basicData.firstName, + lastName: contactData.basicData.lastName, + phoneNumber: contactData.basicData.phoneNumbers.first?.value ?? "", + label: contactData.basicData.phoneNumbers.first?.label ?? "mobile", + present: { [weak parentController] c, a in + parentController?.present(c, in: .window(.root), with: a) + }, + pushController: { c in + push(c) + }, + completed: { + } + ) })) ) items.append( diff --git a/submodules/PeerInfoUI/Sources/UserInfoController.swift b/submodules/PeerInfoUI/Sources/UserInfoController.swift index be7a1488d0..66f16dbb5b 100644 --- a/submodules/PeerInfoUI/Sources/UserInfoController.swift +++ b/submodules/PeerInfoUI/Sources/UserInfoController.swift @@ -61,6 +61,8 @@ public func openAddPersonContactImpl(context: AccountContext, updatedPresentatio let controller = context.sharedContext.makeNewContactScreen( context: context, peer: peer, + firstName: nil, + lastName: nil, phoneNumber: user.phone, shareViaException: shareViaException, completion: { peer, _, _ in diff --git a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift index ab694dfc60..4c44f3ce28 100644 --- a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift +++ b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift @@ -228,6 +228,7 @@ public protocol ManagedAudioSession: AnyObject { func isActive() -> Signal func isPlaybackActive() -> Signal func isOtherAudioPlaying() -> Bool + func didActivateWithZeroVolume() -> Signal func push(params: ManagedAudioSessionClientParams) -> Disposable func dropAll() @@ -311,6 +312,11 @@ public final class ManagedAudioSessionImpl: NSObject, ManagedAudioSession { private var isActiveValue: Bool = false private var callKitAudioSessionIsActive: Bool = false + private let zeroVolumeEvents = ValuePipe() + public func didActivateWithZeroVolume() -> Signal { + return self.zeroVolumeEvents.signal() + } + override public init() { self.queue = Queue() @@ -1123,6 +1129,22 @@ public final class ManagedAudioSessionImpl: NSObject, ManagedAudioSession { if case .voiceCall = type { //try AVAudioSession.sharedInstance().setPreferredIOBufferDuration(0.005) } + + if AVAudioSession.sharedInstance().outputVolume <= 0.01 { + let queue = self.queue + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { [weak self] in + queue.async { + guard let self else { + return + } + if self.currentTypeAndOutputMode != nil { + if AVAudioSession.sharedInstance().outputVolume <= 0.01 { + self.zeroVolumeEvents.putNext(Void()) + } + } + } + }) + } } catch let error { managedAudioSessionLog("ManagedAudioSession activate error \(error)") } diff --git a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift index f1cd0c2990..e81d670733 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift @@ -74,6 +74,16 @@ public final class ChatInlineSearchResultsListComponent: Component { case monoforumChats(query: String) } + public final class ScrollingState { + fileprivate let entryId: Entry.Id + fileprivate let entryOffset: CGFloat + + fileprivate init(entryId: Entry.Id, entryOffset: CGFloat) { + self.entryId = entryId + self.entryOffset = entryOffset + } + } + public let context: AccountContext public let presentation: Presentation public let peerId: EnginePeer.Id? @@ -81,6 +91,7 @@ public final class ChatInlineSearchResultsListComponent: Component { public let insets: UIEdgeInsets public let inputHeight: CGFloat public let showEmptyResults: Bool + public let initialScrollingState: ScrollingState? public let messageSelected: (EngineMessage) -> Void public let peerSelected: (EnginePeer) -> Void public let loadTagMessages: (MemoryBuffer, MessageIndex?) -> Signal? @@ -97,6 +108,7 @@ public final class ChatInlineSearchResultsListComponent: Component { insets: UIEdgeInsets, inputHeight: CGFloat, showEmptyResults: Bool, + initialScrollingState: ScrollingState?, messageSelected: @escaping (EngineMessage) -> Void, peerSelected: @escaping (EnginePeer) -> Void, loadTagMessages: @escaping (MemoryBuffer, MessageIndex?) -> Signal?, @@ -112,6 +124,7 @@ public final class ChatInlineSearchResultsListComponent: Component { self.insets = insets self.inputHeight = inputHeight self.showEmptyResults = showEmptyResults + self.initialScrollingState = initialScrollingState self.messageSelected = messageSelected self.peerSelected = peerSelected self.loadTagMessages = loadTagMessages @@ -146,7 +159,7 @@ public final class ChatInlineSearchResultsListComponent: Component { return true } - private enum Entry: Equatable, Comparable { + fileprivate enum Entry: Equatable, Comparable { enum Id: Hashable { case peer(EnginePeer.Id) case message(EngineMessage.Id) @@ -373,6 +386,26 @@ public final class ChatInlineSearchResultsListComponent: Component { return result } + public func scrollingState() -> ScrollingState? { + var scrollingState: ScrollingState? + self.listNode.forEachVisibleItemNode { itemNode in + if scrollingState != nil { + return + } + if let itemNode = itemNode as? ChatListItemNode, let item = itemNode.item { + switch item.content { + case let .peer(peerData): + if let message = peerData.messages.first { + scrollingState = ScrollingState(entryId: .message(message.id), entryOffset: itemNode.frame.minY - self.listNode.insets.top) + } + default: + break + } + } + } + return scrollingState + } + func update(component: ChatInlineSearchResultsListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -1127,6 +1160,22 @@ public final class ChatInlineSearchResultsListComponent: Component { } self.hintAnimateListTransition = false + if previousComponent == nil, let initialScrollingState = component.initialScrollingState { + var index = 0 + for entry in contentsState.entries { + if entry.id == initialScrollingState.entryId { + scrollToItem = ListViewScrollToItem( + index: index, + position: .top(initialScrollingState.entryOffset), + animated: false, + curve: .Default(duration: nil), + directionHint: .Up + ) + } + index += 1 + } + } + self.listNode.transaction( deleteIndices: deleteIndices.map { index in return ListViewDeleteItem(index: index, directionHint: nil) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 19fc2d759f..c36c5ab7d9 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -1366,8 +1366,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if let action = strongSelf.gestureRecognized(gesture: .longTap, location: point, recognizer: recognizer) { switch action { case let .action(f): - f.action() - recognizer.cancel() + if let actionWithLongTapRecognizer = f.actionWithLongTapRecognizer { + actionWithLongTapRecognizer(recognizer) + } else { + f.action() + } case let .optionalAction(f): f() recognizer.cancel() @@ -5949,12 +5952,12 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI break case let .url(url): if tapAction.hasLongTapAction { - return .action(InternalBubbleTapAction.Action({ [weak self] in + return .action(InternalBubbleTapAction.Action({}, actionWithLongTapRecognizer: { [weak self] gesture in let cleanUrl = url.url.replacingOccurrences(of: "mailto:", with: "") guard let self, let contentNode = self.contextContentNodeForLink(cleanUrl, rects: rects) else { return } - item.controllerInteraction.longTap(.url(url.url), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) + item.controllerInteraction.longTap(.url(url.url), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?(), gesture: gesture)) }, contextMenuOnLongPress: false)) } else { disableDefaultPressAnimation = true diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift index 6fdb5b8347..efcd885e4c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift @@ -623,10 +623,12 @@ public final class ChatMessageAccessibilityData { public enum InternalBubbleTapAction { public struct Action { public var action: () -> Void + public var actionWithLongTapRecognizer: ((TapLongTapOrDoubleTapGestureRecognizer) -> Void)? public var contextMenuOnLongPress: Bool - public init(_ action: @escaping () -> Void, contextMenuOnLongPress: Bool = false) { + public init(_ action: @escaping () -> Void, actionWithLongTapRecognizer: ((TapLongTapOrDoubleTapGestureRecognizer) -> Void)? = nil, contextMenuOnLongPress: Bool = false) { self.action = action + self.actionWithLongTapRecognizer = actionWithLongTapRecognizer self.contextMenuOnLongPress = contextMenuOnLongPress } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index 931ccd6ec2..af00e36bc7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -253,7 +253,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } else { currentMaxGlyphCount = nil } - let previousGlyphCount = self.textNode.textNode.getGlyphCount() + let previousGlyphCount = self.textNode.textNode.getCharacterToGlyphMapping().count return { item, layoutConstants, _, _, _, _ in let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) @@ -828,7 +828,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { var previousAnimateGlyphCount: Int? if hasDraft || hadDraft { - previousAnimateGlyphCount = strongSelf.textNode.textNode.getGlyphCount() + previousAnimateGlyphCount = strongSelf.textNode.textNode.getCharacterToGlyphMapping().count } strongSelf.textNode.textNode.displaysAsynchronously = !item.presentationData.isPreview @@ -1065,7 +1065,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { private func updateTextRevealAnimation(previousGlyphCount: Int) { var fromCount = previousGlyphCount - let toCount = self.textNode.textNode.getGlyphCount() + let toCount = self.textNode.textNode.getCharacterToGlyphMapping().count let timestamp = CACurrentMediaTime() if let textRevealAnimationState = self.textRevealAnimationState { if textRevealAnimationState.toCount == toCount { @@ -1114,7 +1114,10 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { var requestUpdate = false let glyphCount = textRevealAnimationState.glyphCount(timestamp: timestamp) if let revealGlyphCount = self.textNode.textNode.revealGlyphCount, let cachedLayout = self.textNode.textNode.cachedLayout { - if cachedLayout.sizeForGlyphCount(glyphCount: revealGlyphCount).height != cachedLayout.sizeForGlyphCount(glyphCount: glyphCount).height { + let previousLayout = cachedLayout.layoutForGlyphCount(glyphCount: revealGlyphCount) + let updatedLayout = cachedLayout.layoutForGlyphCount(glyphCount: glyphCount) + + if updatedLayout.size.height != previousLayout.size.height || abs(updatedLayout.size.width - previousLayout.size.width) > 8.0 { if lineUpdateTimeout >= 0.0 { lastLineUpdateTimestamp = timestamp requestUpdate = true diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index d200d532ca..ff9b4d23e4 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -164,12 +164,14 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public var contentNode: ContextExtractedContentContainingNode? public var messageNode: ASDisplayNode? public var progress: Promise? + public var gesture: TapLongTapOrDoubleTapGestureRecognizer? - public init(message: Message? = nil, contentNode: ContextExtractedContentContainingNode? = nil, messageNode: ASDisplayNode? = nil, progress: Promise? = nil) { + public init(message: Message? = nil, contentNode: ContextExtractedContentContainingNode? = nil, messageNode: ASDisplayNode? = nil, progress: Promise? = nil, gesture: TapLongTapOrDoubleTapGestureRecognizer? = nil) { self.message = message self.contentNode = contentNode self.messageNode = messageNode self.progress = progress + self.gesture = gesture } } diff --git a/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/NewContactScreen.swift b/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/NewContactScreen.swift index f4dffd096e..34ae81f6c5 100644 --- a/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/NewContactScreen.swift +++ b/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/NewContactScreen.swift @@ -183,10 +183,10 @@ final class NewContactScreenComponent: Component { } - func updateCountryCode(code: Int32, name: String) { + func updateCountryCode(code: Int32, name: String, phoneNumber: String?) { if let view = self.phoneSection.findTaggedView(tag: self.phoneTag) as? ListItemComponentAdaptor.View { if let itemNode = view.itemNode as? PhoneInputItemNode { - itemNode.updateCountryCode(code: code, name: name) + itemNode.updateCountryCode(code: code, name: name, phoneNumber: phoneNumber) } } } @@ -256,6 +256,7 @@ final class NewContactScreenComponent: Component { let strings = environment.strings var initialCountryCode: Int32? + var initialPhoneNumberWithoutCountryCode: String? var updateFocusTag: Any? if self.component == nil { if let peer = component.initialData.peer { @@ -279,6 +280,9 @@ final class NewContactScreenComponent: Component { } else { updateFocusTag = self.firstNameTag } + if phone.hasPrefix("\(countryCode)") { + initialPhoneNumberWithoutCountryCode = String(phone[phone.index(phone.startIndex, offsetBy: "\(countryCode)".count)...]) + } } else { countryCode = AuthorizationSequenceCountrySelectionController.defaultCountryCode() updateFocusTag = self.phoneTag @@ -456,7 +460,7 @@ final class NewContactScreenComponent: Component { itemGenerator: PhoneInputItem( theme: theme, strings: strings, - value: (initialCountryCode, nil, ""), + value: (initialCountryCode, nil, initialPhoneNumberWithoutCountryCode ?? ""), accessory: phoneAccesory, selectCountryCode: { [weak self] in guard let self, let environment = self.environment, let controller = environment.controller() else { @@ -467,7 +471,7 @@ final class NewContactScreenComponent: Component { guard let self else { return } - self.updateCountryCode(code: Int32(code), name: name) + self.updateCountryCode(code: Int32(code), name: name, phoneNumber: nil) self.activateInput(tag: self.phoneTag) } self.deactivateInput() @@ -616,9 +620,8 @@ final class NewContactScreenComponent: Component { contentHeight += sectionSpacing if let initialCountryCode { - self.updateCountryCode(code: initialCountryCode, name: "") + self.updateCountryCode(code: initialCountryCode, name: "", phoneNumber: initialPhoneNumberWithoutCountryCode) } - var optionsSectionItems: [AnyComponentWithIdentity] = [ AnyComponentWithIdentity(id: "syncContact", component: AnyComponent(ListActionItemComponent( @@ -1033,22 +1036,24 @@ public class NewContactScreen: ViewControllerComponentContainer { public static func initialData( peer: EnginePeer? = nil, + firstName: String? = nil, + lastName: String? = nil, phoneNumber: String? = nil, shareViaException: Bool = false ) -> InitialData { if case let .user(user) = peer { return InitialData( peer: peer, - firstName: user.firstName, - lastName: user.lastName, + firstName: firstName ?? user.firstName, + lastName: lastName ?? user.lastName, phoneNumber: user.phone ?? phoneNumber, shareViaException: shareViaException ) } else { return InitialData( peer: nil, - firstName: nil, - lastName: nil, + firstName: firstName, + lastName: lastName, phoneNumber: phoneNumber, shareViaException: false ) diff --git a/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/PhoneInputItem.swift b/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/PhoneInputItem.swift index b87ce20240..475a4d4f54 100644 --- a/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/PhoneInputItem.swift +++ b/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/PhoneInputItem.swift @@ -296,8 +296,8 @@ final class PhoneInputItemNode: ListViewItemNode, ItemListItemNode { self.phoneInputNode.codeAndNumber = self.phoneInputNode.codeAndNumber } - func updateCountryCode(code: Int32, name: String) { - self.phoneInputNode.codeAndNumber = (code, name, self.phoneInputNode.codeAndNumber.2) + func updateCountryCode(code: Int32, name: String, phoneNumber: String? = nil) { + self.phoneInputNode.codeAndNumber = (code, name, phoneNumber ?? self.phoneInputNode.codeAndNumber.2) let _ = self.processNumberChange(self.phoneInputNode.number) } diff --git a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift index 596a7eb76b..c4ffa9609a 100644 --- a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift +++ b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift @@ -418,6 +418,7 @@ public final class InteractiveTextNodeLayout: NSObject { fileprivate let textStroke: (UIColor, CGFloat)? public let displayContentsUnderSpoilers: Bool fileprivate let expandedBlocks: Set + fileprivate var characterToGlyphMapping: [Int]? fileprivate init( attributedString: NSAttributedString?, @@ -1045,9 +1046,22 @@ public final class InteractiveTextNodeLayout: NSObject { if !self.segments.isEmpty, let line = self.segments[0].lines.first { height = line.frame.maxY } + + var actualCount = 0 + var itemCount = 0 + for item in self.getCharacterToGlyphMapping() { + if itemCount >= glyphCount { + break + } + actualCount = item + itemCount += 1 + } + let glyphCount = actualCount + var width: CGFloat = 0.0 var count = 0 var trailingLineWidth: CGFloat = 0.0 + var lineCount = 0 for segment in self.segments { for line in segment.lines { if count >= glyphCount { @@ -1094,13 +1108,18 @@ public final class InteractiveTextNodeLayout: NSObject { } width = max(width, lineWidth) - trailingLineWidth = lineWidth + trailingLineWidth = lineWidth + self.insets.left + self.insets.right + 4.0 + lineCount += 1 } } - let _ = width + width = ceil(width) + self.insets.left + self.insets.right + if lineCount > 1 { + width = self.size.width + } + height += self.insets.top + self.insets.bottom + 2.0 return TextNodeLayout.LayoutInfo( - size: CGSize(width: self.size.width, height: ceil(height)), + size: CGSize(width: width, height: ceil(height)), trailingLineWidth: trailingLineWidth ) } @@ -1108,6 +1127,62 @@ public final class InteractiveTextNodeLayout: NSObject { public func sizeForGlyphCount(glyphCount: Int) -> CGSize { return self.layoutForGlyphCount(glyphCount: glyphCount).size } + + public func getCharacterToGlyphMapping() -> [Int] { + guard let attributedString = self.attributedString else { + return [] + } + if let value = self.characterToGlyphMapping { + return value + } + + let nsString = attributedString.string as NSString + let fullLength = nsString.length + + // Build a map from each composed character start index to its cumulative glyph count. + // Walk glyphs in reveal order and record the glyph count after processing each composed character. + var glyphCountByComposedStart: [Int: Int] = [:] + var cumulativeGlyphCount = 0 + + for segment in self.segments { + for line in segment.lines { + let glyphRuns = CTLineGetGlyphRuns(line.line) as NSArray + for run in glyphRuns { + let run = run as! CTRun + let glyphCount = CTRunGetGlyphCount(run) + + var stringIndices = [CFIndex](repeating: 0, count: glyphCount) + CTRunGetStringIndices(run, CFRangeMake(0, glyphCount), &stringIndices) + + for i in 0.. [Int] { + guard let cachedLayout = self.cachedLayout else { + return [] + } + return cachedLayout.getCharacterToGlyphMapping() + } + public func updateRevealGlyphCount(count: Int?) { guard let cachedLayout = self.cachedLayout else { return @@ -2187,7 +2269,18 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn self.revealGlyphCount = count - if let count { + if var count { + var actualCount = 0 + var itemCount = 0 + for item in self.getCharacterToGlyphMapping() { + if itemCount >= count { + break + } + actualCount = item + itemCount += 1 + } + count = actualCount + var nextItemId = 0 var currentCount = 0 for segment in cachedLayout.segments { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMessage.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMessage.swift index 2bb6b7f03e..ebaea8f357 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMessage.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMessage.swift @@ -76,7 +76,7 @@ extension PeerInfoScreenNode { }, openAd: { _ in }, addContact: { [weak self] phoneNumber in if let strongSelf = self { - strongSelf.context.sharedContext.openAddContact(context: strongSelf.context, firstName: "", lastName: "", phoneNumber: phoneNumber, label: defaultContactLabel, present: { [weak self] controller, arguments in + strongSelf.context.sharedContext.openAddContact(context: strongSelf.context, peer: nil, firstName: "", lastName: "", phoneNumber: phoneNumber, label: defaultContactLabel, present: { [weak self] controller, arguments in self?.controller?.present(controller, in: .window(.root), with: arguments) }, pushController: { [weak self] controller in if let strongSelf = self { diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift index 8831a82eb1..cc9acdf90f 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift @@ -141,7 +141,7 @@ extension ChatControllerImpl { openText = self.presentationData.strings.Conversation_FileOpenIn } - let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer + let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = params.gesture let gesture: ContextGesture? = nil // anyRecognizer as? ContextGesture let source: ContextContentSource diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPhoneContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPhoneContextMenu.swift index 511a8b1711..4441bc44f7 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPhoneContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPhoneContextMenu.swift @@ -79,7 +79,7 @@ extension ChatControllerImpl: MFMessageComposeViewControllerDelegate { ]) let contactData = DeviceContactExtendedData(basicData: basicData, middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") - pushContactContextOptionsController(context: self.context, contextController: c, presentationData: self.presentationData, peer: nil, contactData: contactData, parentController: self, push: { [weak self] c in + pushContactContextOptionsController(context: self.context, contextController: c, presentationData: self.presentationData, peer: peer, contactData: contactData, parentController: self, push: { [weak self] c in self?.push(c) }) })) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 85839d3756..66d9817011 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1405,14 +1405,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if case .current = i { - c.presentationArguments = a - c.statusBar.alphaUpdated = { [weak self] transition in - guard let self else { - return + if c is UndoOverlayController { + self.present(c, in: .current) + } else { + c.presentationArguments = a + c.statusBar.alphaUpdated = { [weak self] transition in + guard let self else { + return + } + self.updateStatusBarPresentation(animated: transition.isAnimated) } - self.updateStatusBarPresentation(animated: transition.isAnimated) + self.galleryPresentationContext.present(c, on: PresentationSurfaceLevel(rawValue: 0), blockInteraction: true, completion: {}) } - self.galleryPresentationContext.present(c, on: PresentationSurfaceLevel(rawValue: 0), blockInteraction: true, completion: {}) } else { self.present(c, in: .window(.root), with: a, blockInteraction: true) } @@ -3617,7 +3621,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }, addContact: { [weak self] phoneNumber in if let strongSelf = self { let _ = strongSelf.presentVoiceMessageDiscardAlert(action: { - strongSelf.context.sharedContext.openAddContact(context: strongSelf.context, firstName: "", lastName: "", phoneNumber: phoneNumber, label: defaultContactLabel, present: { [weak self] controller, arguments in + strongSelf.context.sharedContext.openAddContact(context: strongSelf.context, peer: nil, firstName: "", lastName: "", phoneNumber: phoneNumber, label: defaultContactLabel, present: { [weak self] controller, arguments in self?.present(controller, in: .window(.root), with: arguments) }, pushController: { [weak self] controller in if let strongSelf = self { @@ -9105,6 +9109,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let controller = self.context.sharedContext.makeNewContactScreen( context: self.context, peer: EnginePeer(peer), + firstName: nil, + lastName: nil, phoneNumber: nil, shareViaException: peerStatusSettings.contains(.addExceptionWhenAddingContact), completion: { [weak self] peer, _, _ in diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 3b4906d18d..fb25659d4f 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -211,6 +211,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { var inlineSearchResults: ComponentView? private var inlineSearchResultsReadyDisposable: Disposable? private var inlineSearchResultsReady: Bool = false + private var inlineSearchResultsScrollingState: (domain: ChatSearchDomain, query: String, state: ChatInlineSearchResultsListComponent.ScrollingState)? var isScrollingLockedAtTop: Bool = false @@ -3050,6 +3051,16 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { showNavigateButtons = false } + if let inlineSearchResultsScrollingState = self.inlineSearchResultsScrollingState { + if let search = self.chatPresentationInterfaceState.search { + if search.domain != inlineSearchResultsScrollingState.domain || search.query != inlineSearchResultsScrollingState.query { + self.inlineSearchResultsScrollingState = nil + } + } else { + self.inlineSearchResultsScrollingState = nil + } + } + if displayInlineSearch { let peerId = self.chatPresentationInterfaceState.chatLocation.peerId @@ -3107,6 +3118,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { insets: childContentInsets, inputHeight: layout.inputHeight ?? 0.0, showEmptyResults: self.showListEmptyResults, + initialScrollingState: self.inlineSearchResultsScrollingState?.state, messageSelected: { [weak self] message in guard let self else { return @@ -3385,6 +3397,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { environment: {}, containerSize: layout.size ) + self.inlineSearchResultsScrollingState = nil if let inlineSearchResultsView = inlineSearchResults.view as? ChatInlineSearchResultsListComponent.View { var animateIn = false if inlineSearchResultsView.superview == nil { @@ -3438,6 +3451,11 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { if let inlineSearchResults = self.inlineSearchResults { self.inlineSearchResults = nil if let inlineSearchResultsView = inlineSearchResults.view as? ChatInlineSearchResultsListComponent.View { + + if let search = self.chatPresentationInterfaceState.search, let scrollingState = inlineSearchResultsView.scrollingState() { + self.inlineSearchResultsScrollingState = (search.domain, search.query, scrollingState) + } + transition.updateAlpha(layer: inlineSearchResultsView.layer, alpha: 0.0, completion: { [weak inlineSearchResultsView] _ in inlineSearchResultsView?.removeFromSuperview() }) diff --git a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift index 87181921ff..534654c109 100644 --- a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift @@ -408,7 +408,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { var tapButtonRightInset: CGFloat = rightInset let buttonsContainerSize = CGSize(width: 16.0, height: panelHeight) - self.buttonsContainer.frame = CGRect(origin: CGPoint(x: width - buttonsContainerSize.width - rightInset, y: 0.0), size: buttonsContainerSize) + self.buttonsContainer.frame = CGRect(origin: CGPoint(x: width - buttonsContainerSize.width - rightInset - 4.0, y: 0.0), size: buttonsContainerSize) let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) @@ -608,7 +608,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { let previousMediaReference = self.previousMediaReference let context = self.context - let contentLeftInset: CGFloat = leftInset + 10.0 + let contentLeftInset: CGFloat = leftInset + 18.0 var textLineInset: CGFloat = 10.0 var rightInset: CGFloat = 14.0 + rightInset @@ -707,7 +707,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { var applyImage: (() -> Void)? if let imageDimensions = imageDimensions { let boundingSize = CGSize(width: 35.0, height: 35.0) - applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: 2.0), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())) + applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: 8.0), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())) textLineInset += 9.0 + 35.0 } @@ -885,8 +885,8 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { transition: animationTransition ) - strongSelf.imageNodeContainer.frame = CGRect(origin: CGPoint(x: contentLeftInset + 9.0, y: 8.0), size: CGSize(width: 35.0, height: 35.0)) - strongSelf.imageNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 35.0, height: 35.0)) + strongSelf.imageNodeContainer.frame = CGRect(origin: CGPoint(x: contentLeftInset + 9.0, y: 7.0), size: CGSize(width: 36.0, height: 36.0)) + strongSelf.imageNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 36.0, height: 36.0)) if let applyImage = applyImage { applyImage() diff --git a/submodules/TelegramUI/Sources/ComposeController.swift b/submodules/TelegramUI/Sources/ComposeController.swift index 0d1a5a16fd..e5788e4079 100644 --- a/submodules/TelegramUI/Sources/ComposeController.swift +++ b/submodules/TelegramUI/Sources/ComposeController.swift @@ -202,6 +202,8 @@ public class ComposeControllerImpl: ViewController, ComposeController { let controller = strongSelf.context.sharedContext.makeNewContactScreen( context: strongSelf.context, peer: nil, + firstName: nil, + lastName: nil, phoneNumber: nil, shareViaException: false, completion: { [weak self] peer, stableId, contactData in diff --git a/submodules/TelegramUI/Sources/OpenAddContact.swift b/submodules/TelegramUI/Sources/OpenAddContact.swift index 2a32e11323..d71ffc1abe 100644 --- a/submodules/TelegramUI/Sources/OpenAddContact.swift +++ b/submodules/TelegramUI/Sources/OpenAddContact.swift @@ -8,8 +8,9 @@ import ShareController import AlertUI import PresentationDataUtils import PeerInfoUI +import PhoneNumberFormat -func openAddContactImpl(context: AccountContext, firstName: String = "", lastName: String = "", phoneNumber: String, label: String = "_$!!$_", present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, completed: @escaping () -> Void = {}) { +func openAddContactImpl(context: AccountContext, peer: EnginePeer?, firstName: String = "", lastName: String = "", phoneNumber: String, label: String = "_$!!$_", present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, completed: @escaping () -> Void = {}) { let _ = (DeviceAccess.authorizationStatus(subject: .contacts) |> take(1) |> deliverOnMainQueue).startStandalone(next: { value in @@ -17,8 +18,10 @@ func openAddContactImpl(context: AccountContext, firstName: String = "", lastNam case .allowed: let controller = context.sharedContext.makeNewContactScreen( context: context, - peer: nil, - phoneNumber: phoneNumber, + peer: peer, + firstName: firstName.isEmpty ? nil : firstName, + lastName: lastName.isEmpty ? nil : lastName, + phoneNumber: cleanPhoneNumber(phoneNumber, removePlus: true), shareViaException: false, completion: { peer, stableId, contactData in if let peer = peer { diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index 968abcb521..a75cc7b7c3 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -1,4 +1,6 @@ import Foundation +import MediaPlayer +import AVFAudio import Display import AsyncDisplayKit import Postbox @@ -318,6 +320,45 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { playerType = (file.isVoice || file.isInstantVideo) ? .voice : .file } params.context.sharedContext.mediaManager.setPlaylist((params.context, PeerMessagesMediaPlaylist(context: params.context, location: location, chatLocationContextHolder: params.chatLocationContextHolder)), type: playerType, control: control) + + if case .voice = playerType { + let _ = (params.context.sharedContext.mediaManager.audioSession.didActivateWithZeroVolume() |> map { _ -> Bool in + return true + } |> take(1) |> timeout(1.0, queue: .mainQueue(), alternate: .single(false)) |> deliverOnMainQueue).startStandalone(next: { value in + if value { + let presentationData = params.context.sharedContext.currentPresentationData.with { $0 } + //TODO:localize + let toastController = UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: "Device is muted.", timeout: 4.0, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in + return true + }) + params.present(toastController, nil, .current) + let isNotMuted: Signal = Signal { subscriber in + subscriber.putNext(AVAudioSession.sharedInstance().outputVolume > 0.01) + subscriber.putCompletion() + return EmptyDisposable + } + let _ = isNotMuted + + let volumeView = MPVolumeView() + if let slider = volumeView.subviews.first(where: { $0 is UISlider }) as? UISlider { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + let value = AVAudioSession.sharedInstance().outputVolume + slider.setValue(AVAudioSession.sharedInstance().outputVolume + 0.01, animated: false) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { + slider.setValue(value, animated: false) + } + } + } + + let _ = (isNotMuted |> filter({ $0 }) |> delay(0.1, queue: .mainQueue()) |> restart |> take(1) |> timeout(5.0, queue: .mainQueue(), alternate: .single(false)) |> deliverOnMainQueue).startStandalone(next: { [weak toastController] value in + if value { + toastController?.dismiss() + } + }) + } + }) + } + return true case let .story(storyController): params.dismissInput() diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index e3c040419d..2231f78cbd 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -765,6 +765,7 @@ func openResolvedUrlImpl( if case .new = section { context.sharedContext.openAddContact( context: context, + peer: nil, firstName: "", lastName: "", phoneNumber: "", @@ -841,6 +842,7 @@ func openResolvedUrlImpl( case .contact: context.sharedContext.openAddContact( context: context, + peer: nil, firstName: "", lastName: "", phoneNumber: "", diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 85a08a8197..dd5d28ed28 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2323,8 +2323,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return LocalizationListController(context: context) } - public func openAddContact(context: AccountContext, firstName: String, lastName: String, phoneNumber: String, label: String, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, completed: @escaping () -> Void) { - openAddContactImpl(context: context, firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, label: label, present: present, pushController: pushController, completed: completed) + public func openAddContact(context: AccountContext, peer: EnginePeer?, firstName: String, lastName: String, phoneNumber: String, label: String, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, completed: @escaping () -> Void) { + openAddContactImpl(context: context, peer: peer, firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, label: label, present: present, pushController: pushController, completed: completed) } public func openAddPersonContact(context: AccountContext, peerId: PeerId, pushController: @escaping (ViewController) -> Void, present: @escaping (ViewController, Any?) -> Void) { @@ -4341,8 +4341,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return ChannelMembersSearchControllerImpl(params: params) } - public func makeNewContactScreen(context: AccountContext, peer: EnginePeer?, phoneNumber: String?, shareViaException: Bool, completion: @escaping (EnginePeer?, DeviceContactStableId?, DeviceContactExtendedData?) -> Void) -> ViewController { - return NewContactScreen(context: context, initialData: NewContactScreen.initialData(peer: peer, phoneNumber: phoneNumber, shareViaException: shareViaException), completion: completion) + public func makeNewContactScreen(context: AccountContext, peer: EnginePeer?, firstName: String?, lastName: String?, phoneNumber: String?, shareViaException: Bool, completion: @escaping (EnginePeer?, DeviceContactStableId?, DeviceContactExtendedData?) -> Void) -> ViewController { + return NewContactScreen(context: context, initialData: NewContactScreen.initialData(peer: peer, firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, shareViaException: shareViaException), completion: completion) } public func makeLoginEmailSetupController(context: AccountContext, blocking: Bool, emailPattern: String?, canAutoDismissIfNeeded: Bool, navigationController: NavigationController?, completion: @escaping () -> Void, dismiss: @escaping () -> Void) -> ViewController { From 823244e49d8f45a4b0afe77262a27f40fa23fb8b Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 3 Apr 2026 22:23:39 +0800 Subject: [PATCH 21/50] Remove firebase --- .gitlab-ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 47725bea31..7cc5c7606d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -27,7 +27,6 @@ internal: - rm -rf build-input/configuration-repository-workdir - rm -rf build-input/configuration-repository - python3 -u build-system/Make/Make.py remote-build --darwinContainers="$DARWIN_CONTAINERS" --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="$TELEGRAM_PRIVATE_DATA_PATH/build-configurations/enterprise-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=enterprise --configuration=release_arm64 - - python3 -u build-system/Make/DeployToFirebase.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/firebase-configurations/firebase-enterprise.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip" - python3 -u build-system/Make/DeployBuild.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/deploy-configurations/enterprise-configuration.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip" environment: name: internal From 4235d7e9c89a7cb839bf7bc2b6deab9db01510e4 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 3 Apr 2026 22:40:26 +0800 Subject: [PATCH 22/50] Clean --- .gitlab-ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7cc5c7606d..d30916ab33 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -22,7 +22,6 @@ internal: - export PATH=/opt/homebrew/opt/ruby/bin:$PATH - export PATH=`gem environment gemdir`/bin:$PATH - python3 -u build-system/Make/Make.py remote-build --darwinContainers="$DARWIN_CONTAINERS" --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="build-system/appcenter-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=adhoc --configuration=release_arm64 - - python3 -u build-system/Make/DeployToFirebase.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/firebase-configurations/firebase-internal.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip" - python3 -u build-system/Make/DeployBuild.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/deploy-configurations/internal-configuration.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip" - rm -rf build-input/configuration-repository-workdir - rm -rf build-input/configuration-repository From 65872a4a5903b6f2d557ed789a10642bbcf3102e Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 4 Apr 2026 10:11:30 +0200 Subject: [PATCH 23/50] Poll improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 5 + .../Sources/ComposePollScreen.swift | 126 +++++++++++------- .../TelegramUI/Sources/ChatController.swift | 1 + 3 files changed, 85 insertions(+), 47 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 9e0743a0fa..2ba0e5b0b8 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -16169,3 +16169,8 @@ Error: %8$@"; "PeerInfo.UnofficialSecurityRisk" = "%@ uses an unofficial Telegram client – messages to this user may be less secure."; "Gallery.Live" = "LIVE"; + +"CreatePoll.QuestionNeeded" = "Enter a question"; +"CreatePoll.OptionsNeeded" = "Add at least two options"; +"CreatePoll.QuizCorrectOptionNeeded" = "Select a correct option"; +"CreatePoll.QuizCorrectOptionNeededMultiple" = "Select at least one correct option"; diff --git a/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift b/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift index a76df13afe..07a29f075e 100644 --- a/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift +++ b/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift @@ -450,13 +450,16 @@ final class ComposePollScreenComponent: Component { } } - private var effectiveIsMultiAnswer: Bool { + fileprivate var effectiveIsMultiAnswer: Bool { return self.isMultiAnswer ?? false } enum ValidatedInput { case ready(ComposedPoll) case isUploading + case questionNeeded + case optionsNeeded + case quizCorrectOptionNeeded } var hasAnyData: Bool { @@ -483,9 +486,9 @@ final class ComposePollScreenComponent: Component { return false } - func validatedInput() -> ValidatedInput? { + func validatedInput() -> ValidatedInput { if self.pollTextInputState.text.length == 0 { - return nil + return .questionNeeded } let mappedKind: TelegramMediaPollKind @@ -530,7 +533,7 @@ final class ComposePollScreenComponent: Component { } if mappedOptions.count < 2 { - return nil + return .optionsNeeded } var mappedCorrectAnswers: [Data]? @@ -538,7 +541,7 @@ final class ComposePollScreenComponent: Component { if !selectedQuizOptions.isEmpty { mappedCorrectAnswers = selectedQuizOptions } else { - return nil + return .quizCorrectOptionNeeded } } @@ -1239,7 +1242,7 @@ final class ComposePollScreenComponent: Component { self.environment = environment let theme = environment.theme.withModalBlocksBackground() - + var isChannel = false if case let .channel(channel) = component.peer, case .broadcast = channel.info { isChannel = true @@ -1414,6 +1417,10 @@ final class ComposePollScreenComponent: Component { self.cachedDurationIcon = renderSettingsIcon(name: "Item List/Icons/Timer", backgroundColors: [UIColor(rgb: 0xFF453A)]) self.cachedEmptyIcon = generateSingleColorImage(size: CGSize(width: 30.0, height: 30.0), color: .clear) + + if let isQuiz = component.isQuiz, isQuiz { + self.bottomEdgeEffectView.isHidden = true + } } self.component = component @@ -2581,31 +2588,6 @@ final class ComposePollScreenComponent: Component { self.scrollView.verticalScrollIndicatorInsets = scrollInsets } - let title = self.isQuiz ? environment.strings.CreatePoll_QuizTitle : environment.strings.CreatePoll_Title - let titleSize = self.title.update( - transition: .immediate, - component: AnyComponent( - MultilineTextComponent( - text: .plain( - NSAttributedString( - string: title, - font: Font.semibold(17.0), - textColor: environment.theme.rootController.navigationBar.primaryTextColor - ) - ) - ) - ), - environment: {}, - containerSize: CGSize(width: 200.0, height: 40.0) - ) - let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: floorToScreenPixels((environment.navigationHeight - titleSize.height) / 2.0) + 3.0), size: titleSize) - if let titleView = self.title.view { - if titleView.superview == nil { - component.overNavigationContainer.addSubview(titleView) - } - transition.setFrame(view: titleView, frame: titleFrame) - } - let barButtonSize = CGSize(width: 44.0, height: 44.0) let cancelButtonSize = self.cancelButton.update( transition: transition, @@ -2640,11 +2622,8 @@ final class ComposePollScreenComponent: Component { let validatedInput = self.validatedInput() var isValid = false - var isUploading = false if case .ready = validatedInput { isValid = true - } else if case .isUploading = validatedInput { - isUploading = true } let doneButtonSize = self.doneButton.update( @@ -2654,7 +2633,7 @@ final class ComposePollScreenComponent: Component { backgroundColor: isValid ? environment.theme.list.itemCheckColors.fillColor : environment.theme.list.itemCheckColors.fillColor.desaturated().withMultipliedAlpha(0.5), isDark: environment.theme.overallDarkAppearance, state: .tintedGlass, - isEnabled: isValid || isUploading, + isEnabled: true, component: AnyComponentWithIdentity(id: "done", component: AnyComponent( Text(text: environment.strings.MediaPicker_Send, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor) )), @@ -2676,6 +2655,35 @@ final class ComposePollScreenComponent: Component { transition.setFrame(view: doneButtonView, frame: doneButtonFrame) } + let title = self.isQuiz ? environment.strings.CreatePoll_QuizTitle : environment.strings.CreatePoll_Title + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain( + NSAttributedString( + string: title, + font: Font.semibold(17.0), + textColor: environment.theme.rootController.navigationBar.primaryTextColor + ) + ) + ) + ), + environment: {}, + containerSize: CGSize(width: 200.0, height: 40.0) + ) + var titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: floorToScreenPixels((environment.navigationHeight - titleSize.height) / 2.0) + 3.0), size: titleSize) + if titleFrame.maxX > doneButtonFrame.minX - 16.0 { + titleFrame.origin.x = cancelButtonFrame.maxX + floorToScreenPixels((doneButtonFrame.minX - cancelButtonFrame.maxX) - titleSize.width) / 2.0 + } + + if let titleView = self.title.view { + if titleView.superview == nil { + component.overNavigationContainer.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + if let recenterOnTag { if let targetView = self.collectTextInputStates().first(where: { $0.view.currentTag === recenterOnTag })?.view { let caretRect = targetView.convert(targetView.bounds, to: self.scrollView) @@ -2884,22 +2892,46 @@ public class ComposePollScreen: ViewControllerComponentContainer, AttachmentCont guard let componentView = self.node.hostView.componentView as? ComposePollScreenComponent.View else { return } + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let validatedInput = componentView.validatedInput() - if case let .ready(poll) = validatedInput { + + let title: String? + let text: String + + switch validatedInput { + case let .ready(poll): self.completion(poll) self.dismiss() - } else if case .isUploading = validatedInput { - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let controller = UndoOverlayController( - presentationData: presentationData, - content: .info(title: presentationData.strings.CreatePoll_MediaUploading_Title, text: presentationData.strings.CreatePoll_MediaUploading_Text, timeout: nil, customUndoText: nil), - position: .top, - action: { _ in - return false - } - ) - self.present(controller, in: .current) + return + case .isUploading: + title = presentationData.strings.CreatePoll_MediaUploading_Title + text = presentationData.strings.CreatePoll_MediaUploading_Text + case .questionNeeded: + title = nil + text = presentationData.strings.CreatePoll_QuestionNeeded + case .optionsNeeded: + title = nil + text = presentationData.strings.CreatePoll_OptionsNeeded + case .quizCorrectOptionNeeded: + title = nil + if componentView.effectiveIsMultiAnswer { + text = presentationData.strings.CreatePoll_QuizCorrectOptionNeededMultiple + } else { + text = presentationData.strings.CreatePoll_QuizCorrectOptionNeeded + } } + + let controller = UndoOverlayController( + presentationData: presentationData, + content: .info(title: title, text: text, timeout: nil, customUndoText: nil), + position: .bottom, + action: { _ in + return false + } + ) + self.present(controller, in: .current) + + HapticFeedback().error() } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index b7293406a9..f9331774ec 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -4214,6 +4214,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let _ = self.presentVoiceMessageDiscardAlert(action: { if let controller = self.configurePollCreation(isQuiz: isQuiz) { + controller.navigationPresentation = .modal self.effectiveNavigationController?.pushViewController(controller) } }) From 86e521c3cbb15fd1c625fbadf991f04e8a9607bb Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 7 Apr 2026 01:44:04 +0400 Subject: [PATCH 24/50] Various fixes --- .../Sources/ComposePollScreen.swift | 2 +- .../OverlayAudioPlayerControllerNode.swift | 16 +++------------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift b/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift index 07a29f075e..ab6b1bb0d4 100644 --- a/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift +++ b/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift @@ -532,7 +532,7 @@ final class ComposePollScreenComponent: Component { )) } - if mappedOptions.count < 2 { + if self.isQuiz && mappedOptions.count < 2 { return .optionsNeeded } diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 2fe3644275..9c050cddf5 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -310,12 +310,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu case .file: tagMask = .file } - - var isMusicPlaylist = true - if let playlistLocation = self.playlistLocation as? PeerMessagesPlaylistLocation, case .savedMusic = playlistLocation { - isMusicPlaylist = false - } - + let chatLocationContextHolder = Atomic(value: nil) self.historyNode = ChatHistoryListNodeImpl( context: context, @@ -329,7 +324,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu subject: .message(id: .id(initialMessageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), - mode: .list(reversed: self.currentIsReversed, reverseGroups: !self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch, isMusicPlaylist: isMusicPlaylist), + mode: .list(reversed: self.currentIsReversed, reverseGroups: !self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch, isMusicPlaylist: true), isChatPreview: false, messageTransitionNode: { return nil } @@ -1204,11 +1199,6 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu tagMask = .file } - var isMusicPlaylist = true - if let playlistLocation = self.playlistLocation as? PeerMessagesPlaylistLocation, case .savedMusic = playlistLocation { - isMusicPlaylist = false - } - let chatLocationContextHolder = Atomic(value: nil) let historyNode = ChatHistoryListNodeImpl( context: self.context, @@ -1222,7 +1212,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), - mode: .list(reversed: self.currentIsReversed, reverseGroups: !self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch, isMusicPlaylist: isMusicPlaylist), + mode: .list(reversed: self.currentIsReversed, reverseGroups: !self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch, isMusicPlaylist: true), isChatPreview: false, messageTransitionNode: { return nil } From ae6d05c49d9a545d8b815d8ccd9f550efd4a74ac Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 7 Apr 2026 06:36:13 +0200 Subject: [PATCH 25/50] Various fixes --- Telegram/Telegram-iOS/en.lproj/Localizable.strings | 2 ++ .../Sources/ChatMessageWebpageBubbleContentNode.swift | 5 +++++ .../ComposePollScreen/Sources/ComposePollScreen.swift | 2 +- .../Sources/StarsTransactionsListPanelComponent.swift | 2 ++ 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 2ba0e5b0b8..5fdc64f920 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -16174,3 +16174,5 @@ Error: %8$@"; "CreatePoll.OptionsNeeded" = "Add at least two options"; "CreatePoll.QuizCorrectOptionNeeded" = "Select a correct option"; "CreatePoll.QuizCorrectOptionNeededMultiple" = "Select at least one correct option"; + +"Stars.Intro.Transaction.Commission.Title" = "%@ commission"; diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift index 636399f4ef..12e5aa8f2e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -160,6 +160,11 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent return ChatMessageBubbleContentTapAction(content: .none) } + let incoming = item.message.effectivelyIncoming(item.context.account.peerId) + if incoming && item.associatedData.isSuspiciousPeer { + return ChatMessageBubbleContentTapAction(content: .none) + } + if let file = content.file { if !file.isVideo, !file.isVideoSticker, !file.isAnimated, !file.isAnimatedSticker, !file.isSticker, !file.isMusic { return ChatMessageBubbleContentTapAction(content: .openMessage) diff --git a/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift b/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift index ab6b1bb0d4..709e42b639 100644 --- a/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift +++ b/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift @@ -346,7 +346,7 @@ final class ComposePollScreenComponent: Component { for (id, itemView) in self.pollOptionsSectionContainer.itemViews { if let view = itemView.contents.view as? ListComposePollOptionComponent.View, !view.isRevealed && !view.currentText.isEmpty { let viewFrame = view.convert(view.bounds, to: self.pollOptionsSectionContainer) - let iconFrame = CGRect(origin: CGPoint(x: viewFrame.minX, y: viewFrame.minY), size: CGSize(width: viewFrame.height, height: viewFrame.height)) + let iconFrame = CGRect(origin: CGPoint(x: viewFrame.minX, y: viewFrame.minY), size: CGSize(width: 50.0, height: viewFrame.height)) if iconFrame.contains(localPoint) { return (id, itemView.contents) } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift index 820b5ce56f..94275b3305 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift @@ -378,6 +378,8 @@ final class StarsTransactionsListPanelComponent: Component { itemSubtitle = environment.strings.Stars_Intro_Transaction_Gift_Title } else if let _ = item.subscriptionPeriod { itemSubtitle = environment.strings.Stars_Intro_Transaction_SubscriptionFee_Title + } else if let permille = item.starrefCommissionPermille { + itemSubtitle = environment.strings.Stars_Intro_Transaction_Commission_Title("\(formatPermille(permille))%").string } else { itemSubtitle = nil } From f3e1795b680f6bbd1b56e76a285722fe552f32a5 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 7 Apr 2026 08:11:27 +0200 Subject: [PATCH 26/50] Temp --- Telegram/Tests/Sources/UITests.swift | 2 +- build-system/Make/DeployBuild.py | 32 ++++++++++++++++--- .../Sources/Node/ChatListItem.swift | 3 +- .../Sources/Node/ChatListItemStrings.swift | 4 ++- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/Telegram/Tests/Sources/UITests.swift b/Telegram/Tests/Sources/UITests.swift index 722c782f14..6df5294fe7 100644 --- a/Telegram/Tests/Sources/UITests.swift +++ b/Telegram/Tests/Sources/UITests.swift @@ -31,7 +31,7 @@ class UITests: XCTestCase { } func testSignUp() throws { - deleteTestAccount(phone: "9996629999") + deleteTestAccount(phone: "9996625296") app.launch() // Welcome screen — tap Start Messaging diff --git a/build-system/Make/DeployBuild.py b/build-system/Make/DeployBuild.py index 33b0ae4fdc..db7f0a8bce 100644 --- a/build-system/Make/DeployBuild.py +++ b/build-system/Make/DeployBuild.py @@ -6,6 +6,7 @@ import sys import json import hashlib import base64 +import time import requests def sha256_file(path): @@ -29,13 +30,36 @@ def init_build(host, token, files, channel): def upload_file(path, upload_info): url = upload_info.get('url') headers = dict(upload_info.get('headers', {})) - + size = os.path.getsize(path) headers['Content-Length'] = str(size) - print('Uploading', path) - with open(path, 'rb') as f: - r = requests.put(url, data=f, headers=headers, timeout=900) + print('Uploading {} ({:.1f} MB)'.format(path, size / 1024 / 1024)) + start_time = time.time() + + def reader(): + sent = 0 + last_print = time.time() + with open(path, 'rb') as f: + while True: + chunk = f.read(256 * 1024) + if not chunk: + break + sent += len(chunk) + now = time.time() + if now - last_print >= 5: + elapsed = now - start_time + speed = sent / elapsed / 1024 / 1024 if elapsed > 0 else 0 + print(' {:.1f}% ({:.1f} / {:.1f} MB) {:.2f} MB/s'.format( + sent * 100 / size, sent / 1024 / 1024, size / 1024 / 1024, speed)) + last_print = now + yield chunk + elapsed = time.time() - start_time + speed = size / elapsed / 1024 / 1024 if elapsed > 0 else 0 + print(' 100% - all bytes sent in {:.1f}s ({:.2f} MB/s), waiting for server response...'.format(elapsed, speed)) + + r = requests.put(url, data=reader(), headers=headers, timeout=900) + print(' Server responded: {} ({:.1f}s total)'.format(r.status_code, time.time() - start_time)) if r.status_code != 200: print('Upload failed', r.status_code) print(r.text[:500]) diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 76692de347..af1a1be48c 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -2722,7 +2722,8 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { let messageString: NSAttributedString if !messageText.isEmpty && entities.count > 0 { - messageString = foldLineBreaks(stringWithAppliedEntities(messageText, entities: entities, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat, baseColor: theme.messageTextColor, linkColor: theme.messageTextColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: italicTextFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message._asMessage())) + let appliedString = stringWithAppliedEntities(messageText, entities: entities, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat, baseColor: theme.messageTextColor, linkColor: theme.messageTextColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: italicTextFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message._asMessage()) + messageString = foldLineBreaks(appliedString) } else if spoilers != nil || customEmojiRanges != nil { let mutableString = NSMutableAttributedString(string: messageText, font: textFont, textColor: theme.messageTextColor) if let spoilers = spoilers { diff --git a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift index e82cfbb84d..824887b5d4 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift @@ -287,7 +287,9 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: } if !processed { if !message.text.isEmpty { - messageText = "📎 \(messageText)" + if enableMediaEmoji { + messageText = "📎 \(messageText)" + } } else { if fileMedia.isAnimatedSticker { messageText = strings.Message_Sticker From 96daaaa7949ea72caa8377c6733393eaea08a48b Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 7 Apr 2026 08:36:09 +0200 Subject: [PATCH 27/50] feat: replace iPaddingMbs with bSubcodecMode in SEncParamExt --- .../third_party/openh264/src/codec/api/wels/codec_app_def.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/third-party/openh264/third_party/openh264/src/codec/api/wels/codec_app_def.h b/third-party/openh264/third_party/openh264/src/codec/api/wels/codec_app_def.h index 9b13c33cda..6fd8bd41e5 100644 --- a/third-party/openh264/third_party/openh264/src/codec/api/wels/codec_app_def.h +++ b/third-party/openh264/third_party/openh264/src/codec/api/wels/codec_app_def.h @@ -313,7 +313,10 @@ typedef enum { LEVEL_4_2 = 42, LEVEL_5_0 = 50, LEVEL_5_1 = 51, - LEVEL_5_2 = 52 + LEVEL_5_2 = 52, + LEVEL_6_0 = 60, + LEVEL_6_1 = 61, + LEVEL_6_2 = 62 } ELevelIdc; /** @@ -592,6 +595,7 @@ typedef struct TagEncParamExt { bool bIsLosslessLink; ///< LTR advanced setting bool bFixRCOverShoot; ///< fix rate control overshooting int iIdrBitrateRatio; ///< the target bits of IDR is (idr_bitrate_ratio/100) * average target bit per frame. + bool bSubcodecMode; ///< enable subcodec sprite-compositing constraints (MV cap, no intra in P-frames, no sub-partitions, padding recon override, reduced log2_max_frame_num); default false } SEncParamExt; /** From d224a25d07eef626d901b0153b1ed2dd43100b1b Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 7 Apr 2026 08:36:38 +0200 Subject: [PATCH 28/50] feat: gate MV range cap on bSubcodecMode --- .../openh264/src/codec/encoder/core/src/encoder_ext.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/encoder_ext.cpp b/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/encoder_ext.cpp index 7fdbc614bf..7e0db425d6 100644 --- a/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/encoder_ext.cpp +++ b/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/encoder_ext.cpp @@ -1526,6 +1526,13 @@ void GetMvMvdRange (SWelsSvcCodingParam* pParam, int32_t& iMvRange, int32_t& iMv iMvRange = WELS_MIN (iMvRange, iFixMvRange); + // [subcodec] Limit MV range to 16 pixels for sprite compositing. + // Sprites use a 16px padded canvas; tighter MV range prevents referencing + // pixels outside the padding that will differ in the composite frame. + if (pParam->bSubcodecMode) { + iMvRange = WELS_MIN (iMvRange, 16); + } + iMvdRange = (iMvRange + 1) << 1; iMvdRange = WELS_MIN (iMvdRange, iFixMvdRange); From 1c05425261eba10c177aa6d5fa7c80c63b33cac3 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 7 Apr 2026 08:39:09 +0200 Subject: [PATCH 29/50] feat: gate intra/I_4x4/sub-partition suppression on bSubcodecMode Co-Authored-By: Claude Opus 4.6 (1M context) --- .../encoder/core/src/svc_base_layer_md.cpp | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/svc_base_layer_md.cpp b/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/svc_base_layer_md.cpp index 5acc2d922b..7ba01a86bb 100644 --- a/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/svc_base_layer_md.cpp +++ b/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/svc_base_layer_md.cpp @@ -930,6 +930,11 @@ int32_t WelsMdIntraChroma (SWelsFuncPtrList* pFunc, SDqLayer* pCurDqLayer, SMbCa return iBestCost; } int32_t WelsMdIntraFinePartition (sWelsEncCtx* pEncCtx, SWelsMD* pWelsMd, SMB* pCurMb, SMbCache* pMbCache) { + // [subcodec] I_4x4 disabled for sprite compositing — only I_16x16 allowed. + if (pEncCtx->pSvcParam->bSubcodecMode) { + return pWelsMd->iCostLuma; + } + int32_t iCosti4x4 = WelsMdI4x4 (pEncCtx, pWelsMd, pCurMb, pMbCache); if (iCosti4x4 < pWelsMd->iCostLuma) { @@ -940,6 +945,10 @@ int32_t WelsMdIntraFinePartition (sWelsEncCtx* pEncCtx, SWelsMD* pWelsMd, SMB* p } int32_t WelsMdIntraFinePartitionVaa (sWelsEncCtx* pEncCtx, SWelsMD* pWelsMd, SMB* pCurMb, SMbCache* pMbCache) { + // [subcodec] I_4x4 disabled for sprite compositing — only I_16x16 allowed. + if (pEncCtx->pSvcParam->bSubcodecMode) { + return pWelsMd->iCostLuma; + } if (MdIntraAnalysisVaaInfo (pEncCtx, pMbCache->SPicData.pEncMb[0])) { int32_t iCosti4x4 = WelsMdI4x4Fast (pEncCtx, pWelsMd, pCurMb, pMbCache); @@ -1236,6 +1245,11 @@ int32_t WelsMdP4x8 (SWelsFuncPtrList* pFunc, SDqLayer* pCurDqLayer, SWelsMD* pWe } void WelsMdInterFinePartition (sWelsEncCtx* pEncCtx, SWelsMD* pWelsMd, SSlice* pSlice, SMB* pCurMb, int32_t iBestCost) { + // [subcodec] No sub-partition modes for sprite compositing. + if (pEncCtx->pSvcParam->bSubcodecMode) { + return; + } + SDqLayer* pCurDqLayer = pEncCtx->pCurDqLayer; // SMbCache *pMbCache = &pSlice->sMbCacheInfo; int32_t iCost = 0; @@ -1269,6 +1283,11 @@ void WelsMdInterFinePartition (sWelsEncCtx* pEncCtx, SWelsMD* pWelsMd, SSlice* p void WelsMdInterFinePartitionVaa (sWelsEncCtx* pEncCtx, SWelsMD* pWelsMd, SSlice* pSlice, SMB* pCurMb, int32_t iBestCost) { + // [subcodec] No sub-partition modes for sprite compositing. + if (pEncCtx->pSvcParam->bSubcodecMode) { + return; + } + SDqLayer* pCurDqLayer = pEncCtx->pCurDqLayer; // SMbCache *pMbCache = &pSlice->sMbCacheInfo; int32_t iCostP8x16, iCostP16x8, iCostP8x8; @@ -1827,6 +1846,11 @@ void WelsMdInterMbRefinement (sWelsEncCtx* pEncCtx, SWelsMD* pWelsMd, SMB* pCurM } bool WelsMdFirstIntraMode (sWelsEncCtx* pEncCtx, SWelsMD* pWelsMd, SMB* pCurMb, SMbCache* pMbCache) { + // [subcodec] Never select intra MBs in P-frames for sprite compositing. + if (pEncCtx->pSvcParam->bSubcodecMode) { + return false; + } + SWelsFuncPtrList* pFunc = pEncCtx->pFuncList; int32_t iCostI16x16 = WelsMdI16x16 (pFunc, pEncCtx->pCurDqLayer, pMbCache, pWelsMd->iLambda); From 9871479b27911d5e51a7e87480940589528f246e Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 7 Apr 2026 08:40:23 +0200 Subject: [PATCH 30/50] feat: gate VaaOnScreen sub-partition suppression on bSubcodecMode --- .../src/codec/encoder/core/src/svc_mode_decision.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/svc_mode_decision.cpp b/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/svc_mode_decision.cpp index 5b4793561c..e6541cf9cc 100644 --- a/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/svc_mode_decision.cpp +++ b/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/svc_mode_decision.cpp @@ -611,6 +611,11 @@ bool TryModeMerge (SMbCache* pMbCache, SWelsMD* pWelsMd, SMB* pCurMb) { void WelsMdInterFinePartitionVaaOnScreen (sWelsEncCtx* pEncCtx, SWelsMD* pWelsMd, SSlice* pSlice, SMB* pCurMb, int32_t iBestCost) { + // [subcodec] No sub-partition modes for sprite compositing. + if (pEncCtx->pSvcParam->bSubcodecMode) { + return; + } + SMbCache* pMbCache = &pSlice->sMbCacheInfo; SDqLayer* pCurDqLayer = pEncCtx->pCurDqLayer; int32_t iCostP8x8; From e647f733c9e502541240128b1201fbf4963c7cfb Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 7 Apr 2026 08:41:06 +0200 Subject: [PATCH 31/50] feat: gate padding recon override on bSubcodecMode with hardcoded 1-MB border --- .../encoder/core/src/svc_encode_slice.cpp | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/svc_encode_slice.cpp b/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/svc_encode_slice.cpp index 90139136f9..bff5ac0e9c 100644 --- a/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/svc_encode_slice.cpp +++ b/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/svc_encode_slice.cpp @@ -564,6 +564,33 @@ TRY_REENCODING: if (ENC_RETURN_SUCCESS != iEncReturn) return iEncReturn; + // [subcodec] For padding MBs, overwrite reconstruction buffer with exact black. + // This ensures content MBs' I_16x16 predictions are computed against exact-black + // neighbors, making their coefficients transplantable to the composite frame. + { + if (pEncCtx->pSvcParam->bSubcodecMode) { + const int32_t kiMbX = iCurMbIdx % pCurLayer->iMbWidth; + const int32_t kiMbY = iCurMbIdx / pCurLayer->iMbWidth; + if (kiMbX < 1 || kiMbX >= pCurLayer->iMbWidth - 1 || + kiMbY < 1 || kiMbY >= pCurLayer->iMbHeight - 1) { + // Overwrite luma reconstruction to black (Y=0) + uint8_t* pCsY = pMbCache->SPicData.pCsMb[0]; + const int32_t kiCsStrideY = pCurLayer->iCsStride[0]; + for (int32_t r = 0; r < MB_WIDTH_LUMA; r++) { + memset(pCsY + r * kiCsStrideY, 0, MB_WIDTH_LUMA); + } + // Overwrite chroma reconstruction to neutral (Cb=128, Cr=128) + uint8_t* pCsCb = pMbCache->SPicData.pCsMb[1]; + uint8_t* pCsCr = pMbCache->SPicData.pCsMb[2]; + const int32_t kiCsStrideUV = pCurLayer->iCsStride[1]; + for (int32_t r = 0; r < MB_WIDTH_CHROMA; r++) { + memset(pCsCb + r * kiCsStrideUV, 128, MB_WIDTH_CHROMA); + memset(pCsCr + r * kiCsStrideUV, 128, MB_WIDTH_CHROMA); + } + } + } + } + pCurMb->uiSliceIdc = kiSliceIdx; #if defined(MB_TYPES_CHECK) From d239564d84babe3de6127ff3861091857dd6e07f Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 7 Apr 2026 08:42:28 +0200 Subject: [PATCH 32/50] feat: gate Log2MaxFrameNum on bSubcodecMode via WelsInitSps parameter Co-Authored-By: Claude Sonnet 4.6 --- .../openh264/src/codec/encoder/core/inc/au_set.h | 2 +- .../openh264/src/codec/encoder/core/src/au_set.cpp | 6 +++--- .../src/codec/encoder/core/src/paraset_strategy.cpp | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/third-party/openh264/third_party/openh264/src/codec/encoder/core/inc/au_set.h b/third-party/openh264/third_party/openh264/src/codec/encoder/core/inc/au_set.h index a12106e9cb..b7b4b18af4 100644 --- a/third-party/openh264/third_party/openh264/src/codec/encoder/core/inc/au_set.h +++ b/third-party/openh264/third_party/openh264/src/codec/encoder/core/inc/au_set.h @@ -107,7 +107,7 @@ int32_t WelsWritePpsSyntax (SWelsPPS* pPps, SBitStringAux* pBitStringAux, IWelsP int32_t WelsInitSps (SWelsSPS* pSps, SSpatialLayerConfig* pLayerParam, SSpatialLayerInternal* pLayerParamInternal, const uint32_t kuiIntraPeriod, const int32_t kiNumRefFrame, const uint32_t kiSpsId, const bool kbEnableFrameCropping, bool bEnableRc, - const int32_t kiDlayerCount,bool bSVCBaselayer); + const int32_t kiDlayerCount, bool bSVCBaselayer, bool bSubcodecMode); /*! * \brief initialize subset pSps based on configurable parameters in svc diff --git a/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/au_set.cpp b/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/au_set.cpp index eeac1ec114..78e7523f7a 100644 --- a/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/au_set.cpp +++ b/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/au_set.cpp @@ -485,14 +485,14 @@ static inline bool WelsGetPaddingOffset (int32_t iActualWidth, int32_t iActualHe int32_t WelsInitSps (SWelsSPS* pSps, SSpatialLayerConfig* pLayerParam, SSpatialLayerInternal* pLayerParamInternal, const uint32_t kuiIntraPeriod, const int32_t kiNumRefFrame, const uint32_t kuiSpsId, const bool kbEnableFrameCropping, bool bEnableRc, - const int32_t kiDlayerCount, bool bSVCBaselayer) { + const int32_t kiDlayerCount, bool bSVCBaselayer, bool bSubcodecMode) { memset (pSps, 0, sizeof (SWelsSPS)); pSps->uiSpsId = kuiSpsId; pSps->iMbWidth = (pLayerParam->iVideoWidth + 15) >> 4; pSps->iMbHeight = (pLayerParam->iVideoHeight + 15) >> 4; //max value of both iFrameNum and POC are 2^16-1, in our encoder, iPOC=2*iFrameNum, so max of iFrameNum should be 2^15-1.-- - pSps->uiLog2MaxFrameNum = 15;//16; + pSps->uiLog2MaxFrameNum = bSubcodecMode ? 4 : 15;//16; pSps->iLog2MaxPocLsb = 1 + pSps->uiLog2MaxFrameNum; pSps->iNumRefFrames = kiNumRefFrame; /* min pRef size when fifo pRef operation*/ @@ -565,7 +565,7 @@ int32_t WelsInitSubsetSps (SSubsetSps* pSubsetSps, SSpatialLayerConfig* pLayerPa memset (pSubsetSps, 0, sizeof (SSubsetSps)); WelsInitSps (pSps, pLayerParam, pLayerParamInternal, kuiIntraPeriod, kiNumRefFrame, kuiSpsId, kbEnableFrameCropping, - bEnableRc, kiDlayerCount, false); + bEnableRc, kiDlayerCount, false, false); pSps->uiProfileIdc = pLayerParam->uiProfileIdc ; diff --git a/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/paraset_strategy.cpp b/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/paraset_strategy.cpp index a506362a47..5fb50dcba9 100644 --- a/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/paraset_strategy.cpp +++ b/third-party/openh264/third_party/openh264/src/codec/encoder/core/src/paraset_strategy.cpp @@ -94,7 +94,7 @@ static int32_t WelsGenerateNewSps (sWelsEncCtx* pCtx, const bool kbUseSubsetSps, iRet = WelsInitSps (pSps, pDlayerParam, &pParam->sDependencyLayers[iDlayerIndex], pParam->uiIntraPeriod, pParam->iMaxNumRefFrame, kiSpsId, pParam->bEnableFrameCroppingFlag, pParam->iRCMode != RC_OFF_MODE, iDlayerCount, - bSVCBaselayer); + bSVCBaselayer, pParam->bSubcodecMode); } else { iRet = WelsInitSubsetSps (pSubsetSps, pDlayerParam, &pParam->sDependencyLayers[iDlayerIndex], pParam->uiIntraPeriod, pParam->iMaxNumRefFrame, @@ -178,7 +178,7 @@ int32_t FindExistingSps (SWelsSvcCodingParam* pParam, const bool kbUseSubsetSps, WelsInitSps (&sTmpSps, pDlayerParam, &pParam->sDependencyLayers[iDlayerIndex], pParam->uiIntraPeriod, pParam->iMaxNumRefFrame, 0, pParam->bEnableFrameCroppingFlag, pParam->iRCMode != RC_OFF_MODE, iDlayerCount, - bSVCBaseLayer); + bSVCBaseLayer, pParam->bSubcodecMode); for (int32_t iId = 0; iId < iSpsNumInUse; iId++) { if (CheckMatchedSps (&sTmpSps, &pSpsArray[iId])) { return iId; From 28608194a98a69072567f6d089be03abfe612735 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 7 Apr 2026 08:49:45 +0200 Subject: [PATCH 33/50] fix: copy bSubcodecMode in ParamTranscode so encoder context sees the flag Without this, SWelsSvcCodingParam::ParamTranscode was not copying bSubcodecMode from SEncParamExt, so the encoder's internal pSvcParam always had bSubcodecMode=false regardless of what the caller set. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../third_party/openh264/src/codec/encoder/core/inc/param_svc.h | 1 + 1 file changed, 1 insertion(+) diff --git a/third-party/openh264/third_party/openh264/src/codec/encoder/core/inc/param_svc.h b/third-party/openh264/third_party/openh264/src/codec/encoder/core/inc/param_svc.h index dff33c490a..3a5e6d642c 100644 --- a/third-party/openh264/third_party/openh264/src/codec/encoder/core/inc/param_svc.h +++ b/third-party/openh264/third_party/openh264/src/codec/encoder/core/inc/param_svc.h @@ -345,6 +345,7 @@ typedef struct TagWelsSvcCodingParam: SEncParamExt { bIsLosslessLink = pCodingParam.bIsLosslessLink; bFixRCOverShoot = pCodingParam.bFixRCOverShoot; iIdrBitrateRatio = pCodingParam.iIdrBitrateRatio; + bSubcodecMode = pCodingParam.bSubcodecMode; if (iUsageType == SCREEN_CONTENT_REAL_TIME && !bIsLosslessLink && bEnableLongTermReference) { bEnableLongTermReference = false; } From 1b3a547401cb066d775a7750006b5d2883286cd7 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 7 Apr 2026 09:58:54 +0200 Subject: [PATCH 34/50] subcodec --- .../src/codec/common/inc/wels_common_defs.h | 2 +- .../src/codec/common/src/common_tables.cpp | 8 +- .../src/codec/decoder/core/src/au_parser.cpp | 6 + third-party/subcodec/.gitignore | 10 + third-party/subcodec/CLAUDE.md | 383 ++ third-party/subcodec/CMakeLists.txt | 181 + third-party/subcodec/Package.swift | 142 + .../include/sprite_encode_target.h | 1 + .../Sources/SpriteEncode/sprite_encode.cpp | 1 + .../Sources/SpriteEncode/sprite_extractor.cpp | 1 + .../Sources/SubcodecObjC/AnnexBSplitter.h | 46 + .../Sources/SubcodecObjC/SCDecodedFrame.mm | 22 + .../Sources/SubcodecObjC/SCMuxSurface.mm | 227 + .../Sources/SubcodecObjC/SCOpenH264Decoder.mm | 98 + .../subcodec/Sources/SubcodecObjC/SCSprite.mm | 172 + .../Sources/SubcodecObjC/SCSpriteRegion.mm | 18 + .../SubcodecObjC/SCVideoToolboxDecoder.mm | 306 ++ .../SubcodecObjC/include/SCDecodedFrame.h | 22 + .../Sources/SubcodecObjC/include/SCDecoding.h | 16 + .../SubcodecObjC/include/SCMuxSurface.h | 63 + .../SubcodecObjC/include/SCOpenH264Decoder.h | 17 + .../Sources/SubcodecObjC/include/SCSprite.h | 38 + .../SubcodecObjC/include/SCSpriteRegion.h | 19 + .../include/SCVideoToolboxDecoder.h | 17 + .../SubcodecObjC/include/SubcodecObjC.h | 8 + .../Tests/SubcodecTests/MuxSurfaceTests.swift | 1061 +++++ .../Tests/SubcodecTests/generate_fixtures.cpp | 65 + third-party/subcodec/demo/input.mp4 | Bin 0 -> 1286154 bytes third-party/subcodec/src/cavlc.cpp | 856 ++++ third-party/subcodec/src/cavlc.h | 19 + third-party/subcodec/src/error.h | 15 + third-party/subcodec/src/frame_writer.cpp | 713 +++ third-party/subcodec/src/frame_writer.h | 40 + third-party/subcodec/src/h264_parser.cpp | 684 +++ third-party/subcodec/src/h264_parser.h | 29 + third-party/subcodec/src/mbs_encode.cpp | 627 +++ third-party/subcodec/src/mbs_encode.h | 25 + third-party/subcodec/src/mbs_format.h | 9 + third-party/subcodec/src/mbs_mux_common.cpp | 1004 ++++ third-party/subcodec/src/mbs_mux_common.h | 398 ++ third-party/subcodec/src/mux_surface.cpp | 486 ++ third-party/subcodec/src/mux_surface.h | 118 + third-party/subcodec/src/sprite_data.cpp | 166 + third-party/subcodec/src/sprite_encode.cpp | 291 ++ third-party/subcodec/src/sprite_encode.h | 46 + third-party/subcodec/src/sprite_extractor.cpp | 219 + third-party/subcodec/src/sprite_extractor.h | 39 + third-party/subcodec/src/tables.h | 44 + third-party/subcodec/src/types.h | 115 + .../subcodec/test/test_bs_copy_bits.cpp | 286 ++ third-party/subcodec/test/test_cavlc.cpp | 519 +++ third-party/subcodec/test/test_cavlc_diag.cpp | 287 ++ third-party/subcodec/test/test_cavlc_read.cpp | 114 + .../subcodec/test/test_cavlc_split.cpp | 273 ++ third-party/subcodec/test/test_ct_lut.cpp | 160 + .../subcodec/test/test_ebsp_writer.cpp | 577 +++ third-party/subcodec/test/test_h264_parse.cpp | 356 ++ .../subcodec/test/test_high_profile.cpp | 279 ++ .../subcodec/test/test_idr_frame_ex.cpp | 94 + third-party/subcodec/test/test_ipcm.cpp | 164 + third-party/subcodec/test/test_mb_i16x16.cpp | 347 ++ third-party/subcodec/test/test_mb_p16x16.cpp | 337 ++ third-party/subcodec/test/test_mbs_encode.cpp | 240 + third-party/subcodec/test/test_mbs_format.cpp | 322 ++ third-party/subcodec/test/test_mux.cpp | 484 ++ third-party/subcodec/test/test_mux_alpha.cpp | 569 +++ third-party/subcodec/test/test_mux_perf.cpp | 214 + .../subcodec/test/test_mux_surface.cpp | 682 +++ third-party/subcodec/test/test_p_frame_ex.cpp | 383 ++ .../subcodec/test/test_rbsp_writer.cpp | 188 + third-party/subcodec/test/test_resize.cpp | 966 ++++ third-party/subcodec/test/test_row_plans.cpp | 191 + .../test/test_sprite_encode_alpha.cpp | 50 + .../subcodec/test/test_sprite_extractor.cpp | 113 + .../subcodec/third_party/h264bitstream/bs.h | 388 ++ .../third_party/h264bitstream/h264_analyze.c | 201 + .../third_party/h264bitstream/h264_avcc.c | 142 + .../third_party/h264bitstream/h264_avcc.h | 44 + .../third_party/h264bitstream/h264_nal.c | 340 ++ .../third_party/h264bitstream/h264_sei.c | 696 +++ .../third_party/h264bitstream/h264_sei.h | 180 + .../third_party/h264bitstream/h264_sei.in.c | 276 ++ .../h264bitstream/h264_slice_data.c | 1839 ++++++++ .../h264bitstream/h264_slice_data.h | 129 + .../h264bitstream/h264_slice_data.in.c | 606 +++ .../third_party/h264bitstream/h264_stream.c | 4124 +++++++++++++++++ .../third_party/h264bitstream/h264_stream.h | 707 +++ .../h264bitstream/h264_stream.in.c | 1430 ++++++ .../third_party/h264bitstream/svc_split.c | 188 + .../subcodec/third_party/openh264_codec | 1 + third-party/subcodec/tools/bench_profile.cpp | 419 ++ third-party/subcodec/tools/sprite_extract.cpp | 295 ++ third-party/subcodec/tools/sprite_mux.cpp | 235 + 93 files changed, 29325 insertions(+), 3 deletions(-) create mode 100644 third-party/subcodec/.gitignore create mode 100644 third-party/subcodec/CLAUDE.md create mode 100644 third-party/subcodec/CMakeLists.txt create mode 100644 third-party/subcodec/Package.swift create mode 100644 third-party/subcodec/Sources/SpriteEncode/include/sprite_encode_target.h create mode 100644 third-party/subcodec/Sources/SpriteEncode/sprite_encode.cpp create mode 100644 third-party/subcodec/Sources/SpriteEncode/sprite_extractor.cpp create mode 100644 third-party/subcodec/Sources/SubcodecObjC/AnnexBSplitter.h create mode 100644 third-party/subcodec/Sources/SubcodecObjC/SCDecodedFrame.mm create mode 100644 third-party/subcodec/Sources/SubcodecObjC/SCMuxSurface.mm create mode 100644 third-party/subcodec/Sources/SubcodecObjC/SCOpenH264Decoder.mm create mode 100644 third-party/subcodec/Sources/SubcodecObjC/SCSprite.mm create mode 100644 third-party/subcodec/Sources/SubcodecObjC/SCSpriteRegion.mm create mode 100644 third-party/subcodec/Sources/SubcodecObjC/SCVideoToolboxDecoder.mm create mode 100644 third-party/subcodec/Sources/SubcodecObjC/include/SCDecodedFrame.h create mode 100644 third-party/subcodec/Sources/SubcodecObjC/include/SCDecoding.h create mode 100644 third-party/subcodec/Sources/SubcodecObjC/include/SCMuxSurface.h create mode 100644 third-party/subcodec/Sources/SubcodecObjC/include/SCOpenH264Decoder.h create mode 100644 third-party/subcodec/Sources/SubcodecObjC/include/SCSprite.h create mode 100644 third-party/subcodec/Sources/SubcodecObjC/include/SCSpriteRegion.h create mode 100644 third-party/subcodec/Sources/SubcodecObjC/include/SCVideoToolboxDecoder.h create mode 100644 third-party/subcodec/Sources/SubcodecObjC/include/SubcodecObjC.h create mode 100644 third-party/subcodec/Tests/SubcodecTests/MuxSurfaceTests.swift create mode 100644 third-party/subcodec/Tests/SubcodecTests/generate_fixtures.cpp create mode 100644 third-party/subcodec/demo/input.mp4 create mode 100644 third-party/subcodec/src/cavlc.cpp create mode 100644 third-party/subcodec/src/cavlc.h create mode 100644 third-party/subcodec/src/error.h create mode 100644 third-party/subcodec/src/frame_writer.cpp create mode 100644 third-party/subcodec/src/frame_writer.h create mode 100644 third-party/subcodec/src/h264_parser.cpp create mode 100644 third-party/subcodec/src/h264_parser.h create mode 100644 third-party/subcodec/src/mbs_encode.cpp create mode 100644 third-party/subcodec/src/mbs_encode.h create mode 100644 third-party/subcodec/src/mbs_format.h create mode 100644 third-party/subcodec/src/mbs_mux_common.cpp create mode 100644 third-party/subcodec/src/mbs_mux_common.h create mode 100644 third-party/subcodec/src/mux_surface.cpp create mode 100644 third-party/subcodec/src/mux_surface.h create mode 100644 third-party/subcodec/src/sprite_data.cpp create mode 100644 third-party/subcodec/src/sprite_encode.cpp create mode 100644 third-party/subcodec/src/sprite_encode.h create mode 100644 third-party/subcodec/src/sprite_extractor.cpp create mode 100644 third-party/subcodec/src/sprite_extractor.h create mode 100644 third-party/subcodec/src/tables.h create mode 100644 third-party/subcodec/src/types.h create mode 100644 third-party/subcodec/test/test_bs_copy_bits.cpp create mode 100644 third-party/subcodec/test/test_cavlc.cpp create mode 100644 third-party/subcodec/test/test_cavlc_diag.cpp create mode 100644 third-party/subcodec/test/test_cavlc_read.cpp create mode 100644 third-party/subcodec/test/test_cavlc_split.cpp create mode 100644 third-party/subcodec/test/test_ct_lut.cpp create mode 100644 third-party/subcodec/test/test_ebsp_writer.cpp create mode 100644 third-party/subcodec/test/test_h264_parse.cpp create mode 100644 third-party/subcodec/test/test_high_profile.cpp create mode 100644 third-party/subcodec/test/test_idr_frame_ex.cpp create mode 100644 third-party/subcodec/test/test_ipcm.cpp create mode 100644 third-party/subcodec/test/test_mb_i16x16.cpp create mode 100644 third-party/subcodec/test/test_mb_p16x16.cpp create mode 100644 third-party/subcodec/test/test_mbs_encode.cpp create mode 100644 third-party/subcodec/test/test_mbs_format.cpp create mode 100644 third-party/subcodec/test/test_mux.cpp create mode 100644 third-party/subcodec/test/test_mux_alpha.cpp create mode 100644 third-party/subcodec/test/test_mux_perf.cpp create mode 100644 third-party/subcodec/test/test_mux_surface.cpp create mode 100644 third-party/subcodec/test/test_p_frame_ex.cpp create mode 100644 third-party/subcodec/test/test_rbsp_writer.cpp create mode 100644 third-party/subcodec/test/test_resize.cpp create mode 100644 third-party/subcodec/test/test_row_plans.cpp create mode 100644 third-party/subcodec/test/test_sprite_encode_alpha.cpp create mode 100644 third-party/subcodec/test/test_sprite_extractor.cpp create mode 100644 third-party/subcodec/third_party/h264bitstream/bs.h create mode 100644 third-party/subcodec/third_party/h264bitstream/h264_analyze.c create mode 100644 third-party/subcodec/third_party/h264bitstream/h264_avcc.c create mode 100644 third-party/subcodec/third_party/h264bitstream/h264_avcc.h create mode 100755 third-party/subcodec/third_party/h264bitstream/h264_nal.c create mode 100644 third-party/subcodec/third_party/h264bitstream/h264_sei.c create mode 100644 third-party/subcodec/third_party/h264bitstream/h264_sei.h create mode 100644 third-party/subcodec/third_party/h264bitstream/h264_sei.in.c create mode 100644 third-party/subcodec/third_party/h264bitstream/h264_slice_data.c create mode 100644 third-party/subcodec/third_party/h264bitstream/h264_slice_data.h create mode 100644 third-party/subcodec/third_party/h264bitstream/h264_slice_data.in.c create mode 100644 third-party/subcodec/third_party/h264bitstream/h264_stream.c create mode 100644 third-party/subcodec/third_party/h264bitstream/h264_stream.h create mode 100755 third-party/subcodec/third_party/h264bitstream/h264_stream.in.c create mode 100644 third-party/subcodec/third_party/h264bitstream/svc_split.c create mode 120000 third-party/subcodec/third_party/openh264_codec create mode 100644 third-party/subcodec/tools/bench_profile.cpp create mode 100644 third-party/subcodec/tools/sprite_extract.cpp create mode 100644 third-party/subcodec/tools/sprite_mux.cpp diff --git a/third-party/openh264/third_party/openh264/src/codec/common/inc/wels_common_defs.h b/third-party/openh264/third_party/openh264/src/codec/common/inc/wels_common_defs.h index 96ba01a79d..e3ef1b4070 100644 --- a/third-party/openh264/third_party/openh264/src/codec/common/inc/wels_common_defs.h +++ b/third-party/openh264/third_party/openh264/src/codec/common/inc/wels_common_defs.h @@ -44,7 +44,7 @@ namespace WelsCommon { #define CTX_NA 0 #define WELS_CONTEXT_COUNT 460 -#define LEVEL_NUMBER 17 +#define LEVEL_NUMBER 20 typedef struct TagLevelLimits { ELevelIdc uiLevelIdc; // level idc uint32_t uiMaxMBPS; // Max macroblock processing rate(MB/s) diff --git a/third-party/openh264/third_party/openh264/src/codec/common/src/common_tables.cpp b/third-party/openh264/third_party/openh264/src/codec/common/src/common_tables.cpp index 117b28d089..6dfe9536a9 100644 --- a/third-party/openh264/third_party/openh264/src/codec/common/src/common_tables.cpp +++ b/third-party/openh264/third_party/openh264/src/codec/common/src/common_tables.cpp @@ -363,10 +363,14 @@ const SLevelLimits g_ksLevelLimits[LEVEL_NUMBER] = { {LEVEL_5_0, 589824, 22080, 110400, 135000, 135000, -2048, 2047, 2, 16}, /* level 5 */ {LEVEL_5_1, 983040, 36864, 184320, 240000, 240000, -2048, 2047, 2, 16}, /* level 5.1 */ - {LEVEL_5_2, 2073600, 36864, 184320, 240000, 240000, -2048, 2047, 2, 16} /* level 5.2 */ + {LEVEL_5_2, 2073600, 36864, 184320, 240000, 240000, -2048, 2047, 2, 16}, /* level 5.2 */ + + {LEVEL_6_0, 4177920, 139264, 696320, 240000, 240000, -8192, 8191, 2, 16}, /* level 6.0 */ + {LEVEL_6_1, 8355840, 139264, 696320, 480000, 480000, -8192, 8191, 2, 16}, /* level 6.1 */ + {LEVEL_6_2, 16711680, 139264, 696320, 800000, 800000, -8192, 8191, 2, 16} /* level 6.2 */ }; const uint32_t g_kuiLevelMaps[LEVEL_NUMBER] = { - 10, 9, 11, 12, 13, 20, 21, 22, 30, 31, 32, 40, 41, 42, 50, 51, 52 + 10, 9, 11, 12, 13, 20, 21, 22, 30, 31, 32, 40, 41, 42, 50, 51, 52, 60, 61, 62 }; //for cabac /* this table is from Table9-12 to Table 9-24 */ diff --git a/third-party/openh264/third_party/openh264/src/codec/decoder/core/src/au_parser.cpp b/third-party/openh264/third_party/openh264/src/codec/decoder/core/src/au_parser.cpp index 91f89b4374..f74149c462 100644 --- a/third-party/openh264/third_party/openh264/src/codec/decoder/core/src/au_parser.cpp +++ b/third-party/openh264/third_party/openh264/src/codec/decoder/core/src/au_parser.cpp @@ -829,6 +829,12 @@ const SLevelLimits* GetLevelLimits (int32_t iLevelIdx, bool bConstraint3) { return &g_ksLevelLimits[15]; case 52: return &g_ksLevelLimits[16]; + case 60: + return &g_ksLevelLimits[17]; + case 61: + return &g_ksLevelLimits[18]; + case 62: + return &g_ksLevelLimits[19]; default: return NULL; } diff --git a/third-party/subcodec/.gitignore b/third-party/subcodec/.gitignore new file mode 100644 index 0000000000..3d7733fa15 --- /dev/null +++ b/third-party/subcodec/.gitignore @@ -0,0 +1,10 @@ +build/ +build-*/ +.build/ +.swiftpm/ +*.h264 +*.yuv +*.mbs +*.trace/ +Tests/SubcodecTests/Fixtures/ +.DS_Store diff --git a/third-party/subcodec/CLAUDE.md b/third-party/subcodec/CLAUDE.md new file mode 100644 index 0000000000..4753ebbcb4 --- /dev/null +++ b/third-party/subcodec/CLAUDE.md @@ -0,0 +1,383 @@ +# subcodec + +C++23 library that constructs H.264 Baseline/High Profile bitstreams for sprite multiplexing. Composites many small sprite animations into a single H.264 stream decoded by one hardware decoder, using selective macroblock updates (P_16x16, I_16x16, skip) in P-frames. + +## Build + +```bash +cmake -B build && cmake --build build +``` + +- This project lives inside `telegram-ios/third-party/subcodec/` +- CMake 3.16+, C++23 standard (for `std::expected`); OpenH264 compiled as C++11 +- Dependencies: FFmpeg (libavcodec, libavformat, libavutil, libswscale) — required for sprite_extract video input; vendored h264bitstream; OpenH264 encoder + decoder (Telegram's patched copy at the sibling `../openh264/` directory, accessed via symlink `third_party/openh264_codec` — not vendored here) +- Produces: `libsubcodec.a`, 25 test executables, `sprite_extract` tool, `sprite_mux` tool + +### SwiftPM + +```bash +cmake --build build --target fixtures # generate YUV fixtures before running Swift tests +swift build # builds core library + OpenH264 + sprite_encode + ObjC++ wrapper +swift test # runs Swift XCTest suite (12 tests including 160-frame e2e) +``` + +- Swift 5.9+, targets macOS 10.15+/iOS 13+/tvOS 13+ +- SwiftPM targets: `h264bitstream`, `subcodec`, `oh264_common`, `oh264_processing`, `oh264_encoder`, `oh264_decoder`, `sprite_encode`, `SubcodecObjC`, `SubcodecTests` +- OpenH264 is referenced via the `third_party/openh264_codec` symlink pointing to Telegram's sibling `../openh264/` directory +- CMake remains the primary build system; SwiftPM builds alongside it + +## Project Structure + +- `src/types.h` — Core types: `MacroblockData`, `FrameParams`, `MbContext`, `MbsRow`, `MbsEncodedFrame` (merged rows only), `MbsFrame` (`merged_rows` span), `MbsSprite` (bulk data + row storage) +- `src/error.h` — `subcodec::Error` enum class +- `src/tables.h` — Compile-time lookup tables (CBP, block scan order) +- `src/frame_writer.cpp/.h` — `subcodec::frame_writer` namespace: `write_headers` (auto-selects Baseline/High Profile + level from frame size), `write_idr_frame_ex`, `write_p_frame_ex` +- `src/cavlc.cpp/.h` — `subcodec::cavlc` namespace: CAVLC entropy coding and decoding (spec tables, block read/write, nC context) +- `src/h264_parser.cpp/.h` — `subcodec::H264Parser` class: H.264 Baseline CAVLC slice parser with RAII buffers +- `src/mux_surface.cpp/.h` — `subcodec::MuxSurface` class: streaming mux surface with sprite loading/removal/looping, P-frame emission, dynamic resize. `Params` takes `sprite_width`/`sprite_height` in content pixels (padding added internally). `add_sprite` returns `SpriteRegion` (slot index + color/alpha content pixel rects in composite frame); also sets `needs_emit`/`dirty_` so the next `emit_frame_if_needed` emits the sprite's frame 0 introduction. `advance_sprite(slot)` schedules one sprite for emit+advance (sets `needs_emit` flag); `emit_frame_if_needed(sink)` emits a P-frame if any sprites are scheduled, then advances their frame indices (emit-then-advance semantics). `advance_frame(sink)` is a convenience wrapper that schedules all active sprites then calls `emit_frame_if_needed`. `resize` emits new SPS/PPS + all-I_PCM IDR to change grid dimensions mid-stream, compacting active sprites. `check_compaction_opportunity` returns `CompactionInfo` for caller-driven resize decisions +- `src/mbs_mux_common.cpp/.h` — `subcodec::mux` namespace: shared muxing primitives, `EbspWriter` (single-pass direct EBSP output with inline escape checking + zero-run metadata fast path + `flush_bytes` for NEON-accelerated bulk byte writing), `RbspWriter` (branchless RBSP staging writer, no escape checking), `MicroOp` (pre-resolved blob operation), `RowOp`/`CompositeRowPlan` (precomputed composite grid layout), `build_row_plans`, `build_micro_ops`, `write_p_frame_micro` (active P-frame path), `write_p_frame_rbsp` (legacy two-pass path), `write_idr_ipcm` (inline in header — all-I_PCM IDR for resize transitions, single-pass EbspWriter with precomputed black-MB fast path), `write_skip_safe` (splits long mb_skip_runs with dummy P_16x16 zero-residual MBs for VT compatibility), `scan_zero_runs`, `rbsp_to_ebsp_neon`, exp-golomb LUT, row-blob copy, RBSP↔EBSP conversion +- `src/mbs_encode.cpp/.h` — `subcodec::mbs` namespace: MBS binary format encoding with real nC context and zero-run scanning, returns `MbsEncodedFrame` +- `src/mbs_format.h` — MBS binary format constants (MBS6 format). Header: magic `MBS6` (4 bytes) + width_mbs(2) + height_mbs(2) + num_frames(2) + qp(1) + qp_delta_idr(1) + qp_delta_p(1) + flags(1) = 14 bytes +- `src/sprite_data.cpp` — `MbsSprite` file I/O (v6 bulk load/save with pre-merged blobs), `set_frames()` — no runtime merge needed +- `src/sprite_encode.cpp/.h` — `subcodec::SpriteEncoder` class: OpenH264 encode + parse pipeline. `Params` takes content `width`/`height` in pixels (padding added internally). Encodes on a double-wide canvas (color left, alpha right) and returns `EncodeResult{color, alpha}`. +- `src/sprite_extractor.cpp/.h` — `subcodec::SpriteExtractor` class: raw YUV+alpha → padded canvas → SpriteEncoder → .mbs file with pre-merged row blobs. `Params` takes content `sprite_size` in pixels (padding always 16px internally) +- `tools/sprite_extract.cpp` — CLI: video file → `.mbs` via FFmpeg decode + SpriteEncoder +- `tools/sprite_mux.cpp` — CLI: `.mbs` files → composite H.264 stream via MuxSurface +- `third_party/h264bitstream/` — Vendored NAL unit / bitstream primitives (C) +- `third_party/openh264_codec` — Symlink to Telegram's OpenH264 at the sibling `../openh264/` directory (not vendored here; patches documented below) +- `test/` — 25 standalone C++ test programs (no framework) +- `Sources/SubcodecObjC/` — ObjC++ wrapper: SCSprite, SCMuxSurface, SCSpriteRegion, SCResizeResult, SCCompactionInfo, SCOpenH264Decoder, SCVideoToolboxDecoder, SCDecodedFrame, SCDecoding protocol +- `Sources/SpriteEncode/` — SwiftPM wrapper compilation units for sprite_encode.cpp/sprite_extractor.cpp +- `Tests/SubcodecTests/` — Swift XCTest suite with YUV fixtures (3 sprites × 160 frames) +- `docs/plans/` — Design documents +- `docs/superpowers/specs/` — Feature specs + +## Tests + +```bash +cd build && ctest +``` + +Each test is a standalone C++ executable (no framework): +- `test_cavlc` — CAVLC write encoding correctness +- `test_cavlc_read` — CAVLC read/write round-trip across all 5 VLC tables +- `test_cavlc_diag` — CAVLC diagnostic tests +- `test_cavlc_split` — CAVLC split encoding tests +- `test_ct_lut` — Coeff token lookup table tests +- `test_mb_p16x16` — P_16x16 macroblock encoder + MV prediction +- `test_mb_i16x16` — I_16x16 macroblock encoder +- `test_p_frame_ex` — Extended P-frame writer (all MB types) +- `test_h264_parse` — H.264 slice parser round-trip (write -> parse -> verify) +- `test_idr_frame_ex` — Extended IDR frame writer round-trip +- `test_mbs_format` — MBS binary format encoding/decoding +- `test_mbs_encode` — MBS frame encoding +- `test_mux` (requires OpenH264) — End-to-end: 4 sprites x 8 frames, encode -> mux_surface -> decode -> pixel-identical verification +- `test_mux_surface` (requires OpenH264) — Streaming mux surface: staggered sprite add/remove, mid-stream I_16x16 introduction, sprite looping (2-cycle pixel-identical verification), pixel verification +- `test_sprite_extractor` (requires OpenH264) — SpriteExtractor pipeline test +- `test_bs_copy_bits` — Bulk bit copy correctness (aligned, unaligned, random round-trip) +- `test_mux_perf` — Mux performance stress test: 1764 sprites, 160 frames, per-frame timing +- `test_high_profile` (requires OpenH264) — High Profile SPS acceptance + large grid (52K MBs) mux verification +- `test_ebsp_writer` — EbspWriter unit tests: flush_byte EBSP escaping, write_bits accumulation, exp-golomb LUT correctness, copy_blob aligned/unaligned/partial/sequence verification against bs_t+rbsp_to_ebsp reference, fast-path (5-arg copy_blob) correctness at all bit alignments with escape-free and boundary-escape scenarios +- `test_row_plans` — Row plan precomputation correctness: single sprite, 2x2 grid, partial grid with inactive slots, empty grid (all double-wide slots) +- `test_sprite_encode_alpha` (requires OpenH264) — SpriteEncoder double-wide canvas: alpha MB split, cbp_chroma=0 verification +- `test_mux_alpha` (requires OpenH264) — End-to-end alpha mux: 2 sprites with varying alpha, pixel-identical verification of color and alpha regions against independent reference decode +- `test_rbsp_writer` — RbspWriter + rbsp_to_ebsp_neon verification: compares two-pass (RbspWriter → rbsp_to_ebsp_neon) output against single-pass EbspWriter reference at all bit alignments, with escape-triggering patterns and random data +- `test_ipcm` (requires OpenH264) — I_PCM IDR frame round-trip: write all-I_PCM IDR with gradient pattern, decode via OpenH264, pixel-identical verification +- `test_resize` (requires OpenH264) — MuxSurface dynamic resize: compaction info query, grow/shrink resize, error handling (too few slots), frame counter preservation, pixel-identical verification of sprite content across resize + +### Swift Tests (`swift test`) + +**Note:** Generate YUV fixtures before running Swift tests: `cmake --build build --target fixtures` + +- `testDecodedFrameCreation` — SCDecodedFrame data container +- `testSpriteExtractor` — YUV fixture → SpriteExtractor → .mbs round-trip (160 frames) +- `testSpriteEncoder` — YUV fixture → SpriteEncoder → H.264 stream (160 frames) +- `testEncoderDecodeRoundTrip` — Encode → OpenH264 decode → verify dimensions (8 frames) +- `testMuxSurfaceBasic` — MuxSurface create → add sprite → advance frame +- `testStaggeredAddRemove` — **Main e2e test**: 3 sprites × 160 frames, staggered add/remove, pixel-identical verification against independent reference decode +- `testVideoToolboxDecoder` — Encode 8 frames → VideoToolbox decode → verify frame count and dimensions +- `testDecoderCrossComparison` — Encode 8 frames → decode with both OpenH264 and VideoToolbox → pixel-by-pixel YUV comparison with +/-1 tolerance +- `testSpriteLooping` — 1 sprite × 320 frames (2 loops of 160), pixel-identical verification across loop boundary +- `testAdvanceSpriteIndependent` — 2 sprites with independent frame rates via `advanceSpriteAtSlot:` + `emitFrameIfNeededWithSink:`, pixel-identical verification that only advanced sprite progresses +- `testVideoToolboxPartialFill` — Partially-filled grid (10 sprites in 361-slot grid) decoded via VideoToolbox. Regression test for the write_skip_safe workaround — without it, VT rejects long skip_runs in partially-filled grids. +- `testVideoToolboxIPCM` — MuxSurface resize with I_PCM transition frame decoded via VideoToolbox. Verifies VT handles I_PCM IDR + post-resize P-frames. +- `testResizePerformance` — Resize performance: 420 sprites, resize 420→882 slots with real decoded pixels (OpenH264), wall-clock timing with performance gate (<200ms debug, ~5ms release). + +## Sprite Multiplexing Pipeline + +``` +Input video/YUV+alpha -> SpriteEncoder (double-wide padded canvas: color left, alpha right) + -> H264Parser -> MacroblockData (split into color + alpha halves) + -> save to .mbs (MbsSprite, pre-merged color+alpha row blobs per frame) + -> MuxSurface (double-wide grid: color+alpha side-by-side per slot) -> composite H.264 stream + -> hardware decoder (color and alpha regions decoded in a single frame) +``` + +### How compositing works + +1. **SpriteEncoder** (or **sprite_extract** CLI) encodes each sprite on a double-wide black-padded canvas: color left half (sprite content + padding border) and alpha right half (alpha-as-luma + neutral chroma Cb=Cr=128). For a 64x64 sprite with 16px padding, the canvas is 192x96 pixels (12x6 MBs). It parses the OpenH264 NAL output into `MacroblockData` via `H264Parser`, then splits into color (left half MBs) and alpha (right half MBs). Alpha MBs naturally have `cbp_chroma=0` (no chroma residual) since chroma is uniform 128. + +2. **MBS encoding** (`mbs::encode_frame_merged`) serializes `MacroblockData` into the `.mbs` binary format (MBS v6, magic `MBS6`) with **real nC context** (not canonical nC=0) and **pre-merged color+alpha row blobs**. Color and alpha halves are encoded separately, then merged at encode time into a single blob per row: `[color_data][ue(inter_skip)][alpha_data]`, with leading/trailing skips relative to the full double-wide slot width. Alpha is always present — every sprite has both color and alpha data. The CAVLC is encoded with the actual neighbor-derived nC values, computed from the padded sprite canvas context. MBs are grouped into **row blobs**: each row's non-skip MBs are packed into a contiguous bitstream segment with interleaved skip_run exp-golomb codes. Each row's 6-byte header stores `leading_skips`, `trailing_skips`, packed `blob_bit_count` (15-bit count + 1-bit `has_long_zero_run` flag), `leading_zero_bits`, and `trailing_zero_bits`. The zero-run metadata is computed by scanning each merged blob at encode time. This means the CAVLC bitstream is already correct for the composite grid layout (see "Why real-nC works" below). No runtime merge is needed at load or mux time. + +3. **MuxSurface** arranges sprites in a double-wide grid with shared padding borders. Each slot is `sprite_w * 2 - padding` MBs wide with color and alpha side-by-side, sharing a padding border between them. For the IDR frame (frame 0), all MBs are I_16x16 with DC prediction and zero residual (except MB(0,0) which needs a compensating luma DC coefficient since DC prediction defaults to 128 with no neighbors). This produces an all-black reference frame in ~1-2 bytes/MB. For P-frames, `advance_frame` has two phases: (a) `build_micro_ops` walks the precomputed `RowOp` plans and resolves all pointer chains into a flat `MicroOp` array (one entry per merged blob with data — inactive slots and all-skip rows are folded into skip counts); (b) `write_p_frame_micro` iterates the `MicroOp` array in a tight loop — `write_ue(skip)` + `copy_blob(merged_blob)` per op, using `EbspWriter` for inline EBSP escaping. When the blob's `has_long_zero_run` flag is false, `copy_blob` uses a fast path: only 3 boundary bytes go through `flush_byte` for EBSP escape checking, then the interior is bulk-copied (memcpy for aligned, NEON shift+write for non-aligned) with no EBSP checking — this is safe because the absence of 16-bit zero runs is alignment-invariant. No CAVLC re-encoding and no intermediate RBSP buffer needed. The composite grid layout is precomputed into flat `CompositeRowPlan` / `RowOp` arrays at `add_sprite`/`remove_sprite` time. `__builtin_prefetch` hides cache miss latency in both `build_micro_ops` and `write_p_frame_micro`. + +4. New sprites are introduced mid-stream as I_16x16 MBs in P-frames (their frame 0 data), without requiring an IDR reset. + +5. **Dynamic resize** (`MuxSurface::resize`) changes the grid size mid-stream. The caller provides decoded pixels of the last frame. MuxSurface emits new SPS/PPS + an all-I_PCM IDR with sprite pixels remapped to compacted slot positions in the new grid. Active sprites are compacted into slots 0..N-1 with frame counters preserved. Subsequent P-frames continue from the I_PCM reference. `check_compaction_opportunity` returns `CompactionInfo` (active/max slots, current/min grid MBs) so callers can decide when to resize. I_PCM costs 384 bytes/MB (up to 580 with EBSP escaping) — acceptable as a one-shot cost for an infrequent operation. The intermediate YUV planes use `make_unique_for_overwrite` + explicit `memset` (not `std::vector` fill) to avoid debug-mode per-element initialization overhead. The I_PCM IDR writer (`write_idr_ipcm`, inline in `mbs_mux_common.h`) uses single-pass EbspWriter with two paths: **black-MB fast path** (NEON detects all-black MBs Y=0/Cb=Cr=128, memcpy's precomputed EBSP pattern) and **non-black bulk path** (gathers MB samples into contiguous 384-byte buffer, single `flush_bytes` call with NEON-accelerated EBSP escaping). For a 420→882 slot resize (~45K MBs), this runs in ~2ms (Release) / ~16ms (Swift debug). + +### Key design decisions and why + +**Black-padded canvas (sprite + 1 MB border, hardcoded):** +Padding is always exactly 1 MB (16px) — hardcoded throughout the pipeline, not configurable via any API. All user-facing APIs take content-only dimensions; padding is added internally. Motion vectors in P-frames may reference pixels outside the sprite content area. The black padding ensures these references produce identical pixels in both the independent encode and the composite. OpenH264's MV range is capped at 16px (1 MB) to stay within the padding. + +**I_16x16 for IDR background:** +The IDR frame uses all-I_16x16 with DC prediction to establish exact black pixels (Y=0, Cb=128, Cr=128). MB(0,0) has no neighbors so DC prediction defaults to 128 for luma; a compensating luma DC coefficient (`black_dc_level(qp)`) corrects this to Y=0. All other MBs predict 0 from already-decoded black neighbors → zero residual. This produces ~1-2 bytes/MB (vs ~385 bytes/MB with the previous I_PCM approach). Removed sprites become SKIP MBs referencing the black IDR — no active cleanup needed. Sprites loop indefinitely (frame counter wraps to 0 at `num_frames`), replaying their I_16x16 introduction frame at each loop boundary. Sprites are only removed via explicit `remove_sprite()`. + +**Real-nC MBS encoding with row blobs (no CAVLC re-encoding at mux time):** +The 1-MB padding border ensures that the nC context for every content block is identical between the original sprite canvas and the composite grid: +- Content MBs neighbor either other content MBs from the same sprite (identical total_coeff) or SKIP padding (nC=0) +- This is true in both the original encode and the composite layout +- Therefore, CAVLC encoded with real nC on the sprite canvas is already correct for the composite + +The same argument applies to MV prediction (SKIP padding has MV=0 in both contexts). Each row blob (exp-golomb skip runs + CAVLC blocks) can be copied verbatim at mux time via `EbspWriter::copy_blob`. + +**Note:** Chroma DC blocks use nC=-1 (a fixed VLC table regardless of neighbors), so they never need re-encoding. Luma and chroma AC blocks use neighbor-derived nC, which is why real-nC encoding matters. + +**Alpha channel (side-by-side in composite):** +Alpha is required for all sprites — callers pass an alpha plane alongside Y/Cb/Cr (all-255 for opaque). SpriteEncoder encodes color+alpha on a single double-wide OpenH264 canvas (2× padded width × padded height). Alpha uses luma=alpha values with neutral chroma (Cb=Cr=128), producing `cbp_chroma=0` for all alpha MBs — no chroma data stored. In the composite, each slot places color and alpha side-by-side with shared padding: `[pad][color][shared pad][alpha][pad]` = `sprite_w * 2 - padding` MBs per slot. Color and alpha row blobs are pre-merged at encode time (MBS v6) into a single blob per row: `[color_data][ue(inter_skip)][alpha_data]`, with leading/trailing skips relative to the full slot width. The mux loop emits one merged blob per RowOp via a single `copy_blob` call. No runtime merge needed at load or mux time. Removed sprites' alpha regions become Y=0 (transparent) via SKIP referencing the all-black IDR. + +**No intra MBs in P-frames (OpenH264 patched):** +OpenH264 is patched (via `bSubcodecMode`) to prevent intra MB selection in P-slices and restrict all MBs to I_16x16/P_16x16/SKIP during encoding. This ensures P-frame MBs use only P_16x16 and SKIP (inter prediction from reference frame). I_16x16 MBs in P-frames are used only at the composite mux level for introducing new sprites — their I_16x16 data comes from the sprite's original IDR frame, not from OpenH264's P-frame encoding. + +**OpenH264 for both encode and decode:** +Using the same library for encoding (in sprite_extract) and decoding (in test verification) guarantees consistent behavior. Previously used x264+FFmpeg but hit QP mismatches and format inconsistencies. + +**Automatic profile/level selection:** +`write_headers` computes the H.264 level from the frame's MB count and selects Baseline Profile (up to Level 5.2 / 36,864 MBs) or High Profile (Level 6.0 / 139,264 MBs) automatically. High Profile uses CAVLC (not CABAC) and no 8x8 transforms — the bitstream content is identical to Baseline, just the SPS signals High Profile to unlock higher level limits. Required for 4K+ grids. + +## Vendored OpenH264 Patches + +> **Note:** These patches exist in Telegram's OpenH264 at `third-party/openh264/`. Subcodec does not carry its own OpenH264 copy — it uses the sibling directory via the `third_party/openh264_codec` symlink. + +The OpenH264 used by subcodec has patches gated on `SEncParamExt::bSubcodecMode` (default false). When false, all encoder behavior is stock OpenH264. When true, sprite-compositing constraints activate. Set via `eparam.bSubcodecMode = true` before `InitializeExt()`. The flag is copied in `SWelsSvcCodingParam::ParamTranscode` (`encoder/core/inc/param_svc.h`). + +**Conditional patches (bSubcodecMode=true):** + +1. **No intra in P-frames** (`encoder/core/src/svc_base_layer_md.cpp`): `WelsMdFirstIntraMode()` returns false, preventing I_4x4/I_16x16 selection in P-slices. + +2. **No I_4x4 in intra frames** (`encoder/core/src/svc_base_layer_md.cpp`): `WelsMdIntraFinePartition()` and `WelsMdIntraFinePartitionVaa()` skip I_4x4 evaluation — only I_16x16 allowed. + +3. **No sub-partition modes** (`encoder/core/src/svc_base_layer_md.cpp`, `encoder/core/src/svc_mode_decision.cpp`): `WelsMdInterFinePartition()`, `WelsMdInterFinePartitionVaa()`, `WelsMdInterFinePartitionVaaOnScreen()` skip P_8x8/P_16x8/P_8x16 evaluation — only P_16x16/SKIP allowed. + +4. **MV range cap** (`encoder/core/src/encoder_ext.cpp`): `GetMvMvdRange()` clamps `iMvRange` to 16 pixels max, matching the 1-MB padding border size. + +5. **Padding reconstruction override** (`encoder/core/src/svc_encode_slice.cpp`): After encoding each MB, if it falls within the 1-MB border ring, reconstruction buffer is overwritten to exact black (Y=0, Cb=128, Cr=128). Hardcoded 1-MB padding. + +6. **Log2MaxFrameNum = 4** (`encoder/core/src/au_set.cpp`): `WelsInitSps()` sets `uiLog2MaxFrameNum` to 4 (vs 15 stock) to match subcodec's H.264 parser. The bool is passed as a parameter through call sites in `paraset_strategy.cpp`. + +**Unconditional patches (always active):** + +7. **Level 6.0/6.1/6.2 support** (`api/wels/codec_app_def.h`, `common/inc/wels_common_defs.h`, `common/src/common_tables.cpp`, `decoder/core/src/au_parser.cpp`): Added H.264 Level 6.x entries (up to 139,264 MBs) to the level enum, limit table, level map, and decoder lookup. Stock OpenH264 maxes at Level 5.2 (36,864 MBs). + +## Data Flow Details + +### MacroblockData (types.h) + +Stores all data needed to encode a macroblock: +- `mb_type` — MbType::SKIP, P_16x16, I_16x16 +- `mv_x, mv_y` — Motion vector (half-pel, for P_16x16) +- `intra_pred_mode, intra_chroma_mode` — Prediction modes (for I_16x16) +- `luma_dc[16]` — I_16x16 DC coefficients +- `luma_ac[16][15]` — AC coefficients per 4x4 block +- `cb_dc[4], cr_dc[4], cb_ac[4][15], cr_ac[4][15]` — Chroma coefficients +- `cbp_luma, cbp_chroma` — Coded block pattern + +### MbsEncodedFrame (types.h) + +Owned frame data returned by `mbs::encode_frame_merged()`: +- `data` — `vector` raw frame data (row metadata + merged blobs) +- `rows` — `vector` pre-merged row descriptors (color+alpha combined) + +### MbsFrame (types.h) + +View into bulk-owned frame data (used by MbsSprite after load or set_frames): +- `merged_rows` — `span` pre-merged color+alpha rows (slot_w-relative skips). Used by `build_micro_ops` for the P-frame mux path. + +### MbsRow (types.h) + +Per-row blob descriptor (6-byte on-disk header in MBS v6): +- `leading_skips` — Content SKIPs before first non-skip MB in row +- `trailing_skips` — Content SKIPs after last non-skip MB in row +- `blob_bit_count` — Packed uint16_t: [14:0] = total bits in row blob, [15] = `has_long_zero_run` flag +- `leading_zero_bits` — Zero bits at blob start (capped at 255) +- `trailing_zero_bits` — Zero bits at blob end (capped at 255) +- `blob_data` — Pointer into frame data buffer +- `bit_count()` — Accessor returning lower 15 bits of `blob_bit_count` +- `has_long_zero_run()` — Accessor returning top bit (true if any run of ≥16 consecutive zero bits) + +### MbsSprite (types.h) + +Binary MBS format sprite: per-frame `MbsFrame` views for streaming mux. Move-only. Every sprite has both color and alpha data (pre-merged in v6 format). Loaded from `.mbs` via `MbsSprite::load()` (single bulk read + view parse). Built from encoded frames via `set_frames()` which consolidates into bulk storage. Internally owns: `bulk_data_` (pre-merged blob bytes), `all_rows_` (merged MbsRow descriptors). All `MbsFrame` spans point into these. + +Public fields: `width_mbs`, `height_mbs` (padded dimensions in MBs), `num_frames`, `qp`, `qp_delta_idr`, `qp_delta_p`, `frames` (vector of `MbsFrame` views). Padding is always 1 MB — not stored in the format or struct. + +### FrameParams (types.h) + +Frame dimensions and SPS parameters for the H.264 writers: `width_mbs`, `height_mbs`, `qp`, `log2_max_frame_num`, `pic_order_cnt_type`. + +## Mux Performance + +The mux path was heavily optimized. Key results at 1764 sprites (421x211 MB grid, double-wide with alpha): + +| Metric | Value | +|---|---| +| Sprite add (1764) | ~35 ms (0.02 ms/sprite; v6 pre-merged blobs, no runtime merge) | +| Per-frame p50 | 0.18 ms | +| Per-frame avg | 0.18 ms | + +The hot path in `advance_frame` → `write_p_frame_micro` is: +1. `build_micro_ops`: pre-resolve all blob pointers from `row_ops` + slot state into a flat `MicroOp` array (~33% of advance_frame time). Uses pre-merged rows (color+alpha combined at encode time in v6 format). +2. `write_p_frame_micro`: tight loop over MicroOps — `write_ue(skip)` + `copy_blob(merged_blob)` per op, with EbspWriter's inline EBSP escaping (~66% of advance_frame time). With LTO, `copy_blob` is inlined. + +Key optimizations applied: +- **Pre-resolved MicroOps** (`build_micro_ops` walks `RowOp` plans once per frame and resolves all pointer chains `slot → sprite → frame → merged_rows → MbsRow` into a flat `MicroOp` array. The write loop iterates this array with zero pointer chasing, zero branching on inactive slots, and zero overlap conditionals) +- **Pre-merged color/alpha row blobs** (at encode time, `encode_frame_merged` merges each sprite row's color and alpha blobs into a single blob: `[color_data][ue(inter_skip)][alpha_data]`. Stored pre-merged in MBS v6 format — no runtime merge at load or mux time. Halves the number of `copy_blob` calls per frame. Merged row metadata uses slot-relative leading/trailing skips) +- **Single-pass EbspWriter** (`EbspWriter` writes directly to the output buffer with inline EBSP escape checking — no intermediate RBSP buffer, no separate `rbsp_to_ebsp` scan pass) +- **Zero-run metadata fast path** (each row blob is scanned at encode time for runs of ≥16 consecutive zero bits. If none exist, `copy_blob` skips EBSP escape checking for interior bytes — only 3 boundary bytes go through `flush_byte`, then the interior is bulk-copied via memcpy (aligned) or NEON shift+write (non-aligned). This is safe because the absence of 16-bit zero runs is alignment-invariant: bit shifting doesn't create or destroy bits, so no alignment can produce two consecutive `0x00` bytes) +- **NEON-accelerated non-aligned copy** (ARM NEON `vshlq_u8` processes 16 bytes per iteration for bit-shifted blob copies: two loads, two shifts, one OR, one store. Replaces scalar byte-at-a-time shift+write. Only used for escape-free blobs on the non-aligned path) +- **Exp-golomb LUT** (pre-computed bit patterns for values 0-4095 — skip run writes are a single `write_bits` call instead of per-bit `bs_write_ue`) +- **SWAR zero-byte detection** in `copy_blob` fallback path for blobs with long zero runs (8-byte chunks checked for zero bytes via `(v - 0x0101...) & ~v & 0x8080...`; safe regions memcpy'd directly) +- **Bulk sprite loading** (`MbsSprite::load` reads entire file payload in one `fread`, parses view types into it — 3 heap allocs per sprite instead of ~3,200) +- **Pre-built row blobs** eliminate CAVLC parsing at load time and enable row-level bulk copy at mux time +- **Real-nC MBS encoding** (eliminates CAVLC re-encoding at mux time — row blob copy instead) +- **Per-frame allocation reuse** (output buffer in MuxSurface, not per-frame heap allocs) +- **Zero-free buffer allocation** (`buf_` uses `make_unique_for_overwrite` — no zeroing) +- **All-I_16x16 IDR** (~1-2 bytes/MB vs ~385 bytes/MB with I_PCM) — negligible IDR overhead even for 4K+ grids +- **Precomputed row plans** (grid layout is fixed between `add_sprite`/`remove_sprite`. `build_row_plans` precomputes a flat `RowOp` array with skip counts and overlap offsets. The mux loop iterates this plan with zero divisions, bounds checks, or slot_idx computation) +- **Blob prefetch** (`__builtin_prefetch` on the next sprite's `blob_data` pointer hides L2/L3 cache miss latency. Both within-row and cross-row prefetch in both `build_micro_ops` and `write_p_frame_micro`) +- **Lazy row plan rebuild** (row plans are only rebuilt on the first `advance_frame` after `add_sprite`/`remove_sprite`, not on every add/remove. Eliminates O(N²) plan rebuild cost when adding N sprites sequentially) + +**Build note:** For best performance, build with `-DCMAKE_BUILD_TYPE=Release -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON`. LTO enables cross-TU inlining of `copy_blob` into `write_p_frame_micro`, which is critical for performance (~40% improvement over non-LTO Release). + +**Note:** `RbspWriter`, `rbsp_to_ebsp_neon`, and `write_p_frame_rbsp` (two-pass RBSP staging path) remain in the codebase for reference and testing. The active P-frame path is `write_p_frame_micro`. `bs_copy_bits` and `rbsp_to_ebsp` are used by the IDR path (`write_idr_black`), the encode-time merge in `encode_frame_merged`, and test reference comparisons. The resize I_PCM path (`write_idr_ipcm`) uses single-pass EbspWriter directly — no RBSP buffer or `rbsp_to_ebsp` needed. + +## ObjC++ Wrapper (SubcodecObjC) + +Test-quality ObjC++ bridge exposing the C++ API to Swift. `SC` prefix. One protocol and six classes: + +- `SCSprite` — Two modes: **extractor** (`SCSprite.extractor(withSpriteSize:qp:outputPath:)` — raw YUV+alpha → .mbs file via SpriteExtractor) and **encoder** (`SCSprite.encoder(withWidth:height:qp:)` — raw content-size YUV+alpha → NAL data in memory via SpriteEncoder, for reference decode). Note: `finalizeExtraction()` not `finalize()` (avoids NSObject collision). +- `SCMuxSurface` — Wraps MuxSurface. `SCMuxSurface.create(withSpriteWidth:spriteHeight:maxSlots:qp:sink:)` takes content pixels (no padding param). `addSpriteAtPath:error:` returns `SCSpriteRegion *` (slot, colorRect, alphaRect). `advanceSpriteAtSlot:` schedules one sprite for emit+advance. `emitFrameIfNeededWithSink:error:` emits a P-frame if any sprites are scheduled. `advanceFrameWithSink:error:` convenience that schedules all sprites then emits. `resizeToMaxSlots:yPlane:cbPlane:crPlane:...` returns `SCResizeResult *` (array of `SCSpriteRegion`). `checkCompactionOpportunity` returns `SCCompactionInfo *`. +- `SCResizeResult` — Result of `resizeToMaxSlots:`: `regions` array of `SCSpriteRegion` with compacted slot assignments. +- `SCCompactionInfo` — Result of `checkCompactionOpportunity`: `activeSprites`, `maxSlots`, `currentGridMbs`, `minGridMbs`. +- `SCSpriteRegion` — Result of `addSpriteAtPath:error:`: `slot` index, `colorRect` and `alphaRect` as content pixel regions in composite frame. +- `SCDecoding` — Protocol defining shared decoder interface: `createDecoderWithError:` and `decodeStream:error:`. +- `SCOpenH264Decoder` — OpenH264 software decoder. Factory: `SCOpenH264Decoder.createDecoder()`. Conforms to `SCDecoding`. +- `SCVideoToolboxDecoder` — VideoToolbox hardware decoder. Factory: `SCVideoToolboxDecoder.createDecoder()`. Conforms to `SCDecoding`. Converts Annex B → AVCC internally. Outputs NV12, deinterleaves to separate Y/Cb/Cr planes for `SCDecodedFrame`. +- `SCDecodedFrame` — YUV plane data container (width, height, y, cb, cr as NSData). + +## Production Readiness + +### What's production-ready +The core C++ library (`libsubcodec`) — .mbs format, real-nC encoding, MuxSurface compositing with merged-blob micro-op path — is proven correct with pixel-identical verification across 160 frames in both C++ and Swift tests. Stress-tested at 1764 sprites with ~0.18ms/frame p50 mux time (LTO Release). Verified with ffmpeg decode of real sprite data. + +### What's needed for a production Apple app + +1. **VideoToolbox integration (partially done)** — `SCVideoToolboxDecoder` provides bulk synchronous decode via `VTDecompressionSession`. For production, still needs: + - Streaming frame-at-a-time decode timed to display cadence (CADisplayLink) + - Display pipeline (CVPixelBuffer → Metal texture or CALayer) + - The current wrapper copies YUV planes per frame; production should pass CVPixelBuffers directly to the display layer + +2. **Streaming frame API** — Current ObjC++ wrapper writes into pre-allocated buffers. Production needs frame-at-a-time output timed to display cadence (CADisplayLink or similar). + +3. **Encoding can be offline** — SpriteExtractor/.mbs generation can happen at build time or on a server. The app only needs MuxSurface (compositing) + VideoToolbox (decode). OpenH264 encoder need not ship in the app binary. + +4. **Thread safety, error recovery, memory pressure** — Not addressed in current wrapper. + +## Known Issues + +- **OpenH264 OOM at large resolutions:** OpenH264's decoder runs out of memory for frame sizes around 3K+ pixels per dimension. This only affects test verification — production uses VideoToolbox for decode. + +- **I_PCM + I_16x16 mixing in IDR (OpenH264 bug):** OpenH264 returns `dsBitstreamError` when an IDR slice contains a mix of I_PCM and I_16x16 MBs due to `InitReadBits(pBs, 0)` corrupting bit-reader state after I_PCM parsing. The composite IDR uses all-I_16x16 (no mixing). The resize transition IDR uses all-I_PCM (no mixing). Both avoid the bug. + +- **VideoToolbox rejects long skip_runs in partially-filled grids (worked around):** VT's H.264 decoder fails with `kVTVideoDecoderBadDataErr` (-8969) on structurally valid P-frames when large mb_skip_run values (from empty grid rows) interact with certain CAVLC blob patterns. The bitstream passes H264Parser and ffmpeg validation. Worked around by `write_skip_safe`: skip_runs exceeding `MAX_SKIP_RUN` (2048) are split by inserting dummy P_16x16 zero-residual MBs (4 bits each, semantically identical to SKIP). Tested by `testVideoToolboxPartialFill`. + +## Profiling (advance_frame hot path) + +### Tool: `bench_profile` + +`tools/bench_profile.cpp` — parameterized profiling binary for `MuxSurface::advance_frame` and `MuxSurface::resize`. Uses `os_signpost` instrumentation, designed for `xctrace` CLI (non-interactive). + +```bash +# Generate demo .mbs (one-time, requires FFmpeg) +cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build +./build/sprite_extract demo/input.mp4 demo/output0.mbs 64 + +# Build (MUST use Release for meaningful profiles) +cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build --target bench_profile + +# Profile advance_frame with xctrace +xctrace record --template 'Time Profiler' --output profile.trace \ + --launch -- ./build/bench_profile --mbs-path demo/output0.mbs \ + --sprite-count 1764 --frame-count 160 --loops 100 + +# Profile resize (420 active sprites, resize to 882 slots, 50 iterations) +xctrace record --template 'Time Profiler' --output resize.trace \ + --launch -- ./build/bench_profile --mbs-path demo/output0.mbs \ + --profile-resize --resize-from 420 --resize-to 882 --resize-loops 50 + +# Export trace to XML for automated analysis +xctrace export --input profile.trace \ + --xpath '/trace-toc/run[@number="1"]/data/table[@schema="time-profile"]' > profile.xml +``` + +CLI args: `--mbs-path ` or `--input